From 2da6072a05e5374fda6ea602962c509c5bcf0c5d Mon Sep 17 00:00:00 2001 From: josephkurian Date: Thu, 11 Jul 2024 09:53:32 +0530 Subject: [PATCH] pages commit --- .htaccess | 5 + application/.htaccess | 6 + application/cache/index.html | 11 + application/config/autoload.php | 135 + application/config/config.php | 532 ++++ application/config/constants.php | 85 + application/config/database.php | 96 + application/config/doctypes.php | 24 + application/config/foreign_chars.php | 114 + application/config/hooks.php | 13 + application/config/index.html | 11 + application/config/memcached.php | 19 + application/config/migration.php | 84 + application/config/mimes.php | 186 ++ application/config/profiler.php | 14 + application/config/routes.php | 23 + application/config/smileys.php | 64 + application/config/user_agents.php | 222 ++ application/controllers/Categories.php | 68 + application/controllers/Comments.php | 28 + application/controllers/Pages.php | 22 + application/controllers/Posts.php | 145 + application/controllers/Users.php | 106 + application/controllers/Welcome.php | 25 + application/controllers/index.html | 11 + application/core/index.html | 11 + application/helpers/index.html | 11 + application/hooks/index.html | 11 + application/index.html | 11 + application/language/english/index.html | 11 + application/language/index.html | 11 + application/libraries/index.html | 11 + application/logs/index.html | 11 + application/models/Category_model.php | 38 + application/models/Comment_model.php | 24 + application/models/Post_model.php | 71 + application/models/User_model.php | 51 + application/models/index.html | 11 + application/third_party/index.html | 11 + application/views/categories/create.php | 13 + application/views/categories/index.php | 12 + application/views/errors/cli/error_404.php | 8 + application/views/errors/cli/error_db.php | 8 + .../views/errors/cli/error_exception.php | 21 + .../views/errors/cli/error_general.php | 8 + application/views/errors/cli/error_php.php | 21 + application/views/errors/cli/index.html | 11 + application/views/errors/html/error_404.php | 64 + application/views/errors/html/error_db.php | 64 + .../views/errors/html/error_exception.php | 32 + .../views/errors/html/error_general.php | 64 + application/views/errors/html/error_php.php | 33 + application/views/errors/html/index.html | 11 + application/views/errors/index.html | 11 + application/views/index.html | 11 + application/views/pages/about.php | 2 + application/views/pages/home.php | 2 + application/views/posts/create.php | 29 + application/views/posts/edit.php | 26 + application/views/posts/index.php | 22 + application/views/posts/view.php | 48 + application/views/templates/footer.php | 6 + application/views/templates/header.php | 76 + application/views/users/login.php | 14 + application/views/users/register.php | 34 + application/views/welcome_message.php | 100 + assets/css/bootstrap.min.css | 11 + assets/css/style.css | 30 + assets/images/posts/OS_Ubuntu.png | Bin 0 -> 15196 bytes assets/images/posts/angular.png | Bin 0 -> 4509 bytes assets/images/posts/bibimbap.png | Bin 0 -> 53549 bytes assets/images/posts/bitcoin.png | Bin 0 -> 28640 bytes assets/images/posts/bitcoin1.png | Bin 0 -> 28640 bytes assets/images/posts/ci.png | Bin 0 -> 2339 bytes assets/images/posts/codeigniter.jpg | Bin 0 -> 20224 bytes assets/images/posts/download.jpeg | Bin 0 -> 14817 bytes assets/images/posts/noimage.png | Bin 0 -> 3167 bytes assets/images/posts/noimage1.png | Bin 0 -> 3167 bytes assets/images/posts/united-states.png | Bin 0 -> 11661 bytes composer.json | 22 + contributing.md | 95 + index.php | 315 ++ license.txt | 21 + readme.rst | 57 + system/.htaccess | 6 + system/core/Benchmark.php | 133 + system/core/CodeIgniter.php | 556 ++++ system/core/Common.php | 857 +++++ system/core/Config.php | 379 +++ system/core/Controller.php | 96 + system/core/Exceptions.php | 274 ++ system/core/Hooks.php | 266 ++ system/core/Input.php | 897 ++++++ system/core/Lang.php | 203 ++ system/core/Loader.php | 1437 +++++++++ system/core/Log.php | 296 ++ system/core/Model.php | 80 + system/core/Output.php | 848 +++++ system/core/Router.php | 515 +++ system/core/Security.php | 1078 +++++++ system/core/URI.php | 643 ++++ system/core/Utf8.php | 164 + system/core/compat/hash.php | 245 ++ system/core/compat/index.html | 11 + system/core/compat/mbstring.php | 149 + system/core/compat/password.php | 251 ++ system/core/compat/standard.php | 182 ++ system/core/index.html | 11 + system/database/DB.php | 218 ++ system/database/DB_cache.php | 221 ++ system/database/DB_driver.php | 1986 ++++++++++++ system/database/DB_forge.php | 1032 ++++++ system/database/DB_query_builder.php | 2789 +++++++++++++++++ system/database/DB_result.php | 666 ++++ system/database/DB_utility.php | 424 +++ .../database/drivers/cubrid/cubrid_driver.php | 405 +++ .../database/drivers/cubrid/cubrid_forge.php | 230 ++ .../database/drivers/cubrid/cubrid_result.php | 177 ++ .../drivers/cubrid/cubrid_utility.php | 79 + system/database/drivers/cubrid/index.html | 11 + .../database/drivers/ibase/ibase_driver.php | 413 +++ system/database/drivers/ibase/ibase_forge.php | 251 ++ .../database/drivers/ibase/ibase_result.php | 161 + .../database/drivers/ibase/ibase_utility.php | 69 + system/database/drivers/ibase/index.html | 11 + system/database/drivers/index.html | 11 + system/database/drivers/mssql/index.html | 11 + .../database/drivers/mssql/mssql_driver.php | 518 +++ system/database/drivers/mssql/mssql_forge.php | 151 + .../database/drivers/mssql/mssql_result.php | 198 ++ .../database/drivers/mssql/mssql_utility.php | 77 + system/database/drivers/mysql/index.html | 11 + .../database/drivers/mysql/mysql_driver.php | 494 +++ system/database/drivers/mysql/mysql_forge.php | 243 ++ .../database/drivers/mysql/mysql_result.php | 199 ++ .../database/drivers/mysql/mysql_utility.php | 211 ++ system/database/drivers/mysqli/index.html | 11 + .../database/drivers/mysqli/mysqli_driver.php | 544 ++++ .../database/drivers/mysqli/mysqli_forge.php | 244 ++ .../database/drivers/mysqli/mysqli_result.php | 186 ++ .../drivers/mysqli/mysqli_utility.php | 213 ++ system/database/drivers/oci8/index.html | 11 + system/database/drivers/oci8/oci8_driver.php | 688 ++++ system/database/drivers/oci8/oci8_forge.php | 185 ++ system/database/drivers/oci8/oci8_result.php | 229 ++ system/database/drivers/oci8/oci8_utility.php | 68 + system/database/drivers/odbc/index.html | 11 + system/database/drivers/odbc/odbc_driver.php | 425 +++ system/database/drivers/odbc/odbc_forge.php | 86 + system/database/drivers/odbc/odbc_result.php | 268 ++ system/database/drivers/odbc/odbc_utility.php | 63 + system/database/drivers/pdo/index.html | 11 + system/database/drivers/pdo/pdo_driver.php | 375 +++ system/database/drivers/pdo/pdo_forge.php | 65 + system/database/drivers/pdo/pdo_result.php | 198 ++ system/database/drivers/pdo/pdo_utility.php | 63 + .../drivers/pdo/subdrivers/index.html | 11 + .../drivers/pdo/subdrivers/pdo_4d_driver.php | 200 ++ .../drivers/pdo/subdrivers/pdo_4d_forge.php | 217 ++ .../pdo/subdrivers/pdo_cubrid_driver.php | 250 ++ .../pdo/subdrivers/pdo_cubrid_forge.php | 230 ++ .../pdo/subdrivers/pdo_dblib_driver.php | 337 ++ .../pdo/subdrivers/pdo_dblib_forge.php | 149 + .../pdo/subdrivers/pdo_firebird_driver.php | 279 ++ .../pdo/subdrivers/pdo_firebird_forge.php | 237 ++ .../drivers/pdo/subdrivers/pdo_ibm_driver.php | 244 ++ .../drivers/pdo/subdrivers/pdo_ibm_forge.php | 154 + .../pdo/subdrivers/pdo_informix_driver.php | 309 ++ .../pdo/subdrivers/pdo_informix_forge.php | 163 + .../pdo/subdrivers/pdo_mysql_driver.php | 374 +++ .../pdo/subdrivers/pdo_mysql_forge.php | 256 ++ .../drivers/pdo/subdrivers/pdo_oci_driver.php | 326 ++ .../drivers/pdo/subdrivers/pdo_oci_forge.php | 176 ++ .../pdo/subdrivers/pdo_odbc_driver.php | 229 ++ .../drivers/pdo/subdrivers/pdo_odbc_forge.php | 70 + .../pdo/subdrivers/pdo_pgsql_driver.php | 384 +++ .../pdo/subdrivers/pdo_pgsql_forge.php | 210 ++ .../pdo/subdrivers/pdo_sqlite_driver.php | 219 ++ .../pdo/subdrivers/pdo_sqlite_forge.php | 238 ++ .../pdo/subdrivers/pdo_sqlsrv_driver.php | 369 +++ .../pdo/subdrivers/pdo_sqlsrv_forge.php | 149 + system/database/drivers/postgre/index.html | 11 + .../drivers/postgre/postgre_driver.php | 620 ++++ .../drivers/postgre/postgre_forge.php | 205 ++ .../drivers/postgre/postgre_result.php | 182 ++ .../drivers/postgre/postgre_utility.php | 78 + system/database/drivers/sqlite/index.html | 11 + .../database/drivers/sqlite/sqlite_driver.php | 330 ++ .../database/drivers/sqlite/sqlite_forge.php | 205 ++ .../database/drivers/sqlite/sqlite_result.php | 164 + .../drivers/sqlite/sqlite_utility.php | 61 + system/database/drivers/sqlite3/index.html | 11 + .../drivers/sqlite3/sqlite3_driver.php | 350 +++ .../drivers/sqlite3/sqlite3_forge.php | 225 ++ .../drivers/sqlite3/sqlite3_result.php | 194 ++ .../drivers/sqlite3/sqlite3_utility.php | 61 + system/database/drivers/sqlsrv/index.html | 11 + .../database/drivers/sqlsrv/sqlsrv_driver.php | 543 ++++ .../database/drivers/sqlsrv/sqlsrv_forge.php | 149 + .../database/drivers/sqlsrv/sqlsrv_result.php | 193 ++ .../drivers/sqlsrv/sqlsrv_utility.php | 77 + system/database/index.html | 11 + system/fonts/index.html | 11 + system/fonts/texb.ttf | Bin 0 -> 143830 bytes system/helpers/array_helper.php | 115 + system/helpers/captcha_helper.php | 341 ++ system/helpers/cookie_helper.php | 113 + system/helpers/date_helper.php | 741 +++++ system/helpers/directory_helper.php | 101 + system/helpers/download_helper.php | 158 + system/helpers/email_helper.php | 84 + system/helpers/file_helper.php | 453 +++ system/helpers/form_helper.php | 1032 ++++++ system/helpers/html_helper.php | 410 +++ system/helpers/index.html | 11 + system/helpers/inflector_helper.php | 275 ++ system/helpers/language_helper.php | 75 + system/helpers/number_helper.php | 94 + system/helpers/path_helper.php | 82 + system/helpers/security_helper.php | 137 + system/helpers/smiley_helper.php | 255 ++ system/helpers/string_helper.php | 306 ++ system/helpers/text_helper.php | 564 ++++ system/helpers/typography_helper.php | 104 + system/helpers/url_helper.php | 569 ++++ system/helpers/xml_helper.php | 90 + system/index.html | 11 + system/language/english/calendar_lang.php | 84 + system/language/english/date_lang.php | 94 + system/language/english/db_lang.php | 63 + system/language/english/email_lang.php | 58 + .../language/english/form_validation_lang.php | 68 + system/language/english/ftp_lang.php | 51 + system/language/english/imglib_lang.php | 56 + system/language/english/index.html | 11 + system/language/english/migration_lang.php | 47 + system/language/english/number_lang.php | 44 + system/language/english/pagination_lang.php | 43 + system/language/english/profiler_lang.php | 60 + system/language/english/unit_test_lang.php | 58 + system/language/english/upload_lang.php | 55 + system/language/index.html | 11 + system/libraries/Cache/Cache.php | 255 ++ system/libraries/Cache/drivers/Cache_apc.php | 221 ++ .../libraries/Cache/drivers/Cache_dummy.php | 172 + system/libraries/Cache/drivers/Cache_file.php | 286 ++ .../Cache/drivers/Cache_memcached.php | 303 ++ .../libraries/Cache/drivers/Cache_redis.php | 328 ++ .../Cache/drivers/Cache_wincache.php | 217 ++ system/libraries/Cache/drivers/index.html | 11 + system/libraries/Cache/index.html | 11 + system/libraries/Calendar.php | 546 ++++ system/libraries/Cart.php | 567 ++++ system/libraries/Driver.php | 342 ++ system/libraries/Email.php | 2434 ++++++++++++++ system/libraries/Encrypt.php | 480 +++ system/libraries/Encryption.php | 943 ++++++ system/libraries/Form_validation.php | 1584 ++++++++++ system/libraries/Ftp.php | 667 ++++ system/libraries/Image_lib.php | 1830 +++++++++++ system/libraries/Javascript.php | 856 +++++ system/libraries/Javascript/Jquery.php | 1076 +++++++ system/libraries/Javascript/index.html | 11 + system/libraries/Migration.php | 477 +++ system/libraries/Pagination.php | 701 +++++ system/libraries/Parser.php | 248 ++ system/libraries/Profiler.php | 562 ++++ system/libraries/Session/Session.php | 985 ++++++ .../Session/SessionHandlerInterface.php | 59 + system/libraries/Session/Session_driver.php | 191 ++ .../drivers/Session_database_driver.php | 420 +++ .../Session/drivers/Session_files_driver.php | 406 +++ .../drivers/Session_memcached_driver.php | 378 +++ .../Session/drivers/Session_redis_driver.php | 400 +++ system/libraries/Session/drivers/index.html | 11 + system/libraries/Session/index.html | 11 + system/libraries/Table.php | 538 ++++ system/libraries/Trackback.php | 556 ++++ system/libraries/Typography.php | 424 +++ system/libraries/Unit_test.php | 406 +++ system/libraries/Upload.php | 1318 ++++++++ system/libraries/User_agent.php | 681 ++++ system/libraries/Xmlrpc.php | 1922 ++++++++++++ system/libraries/Xmlrpcs.php | 625 ++++ system/libraries/Zip.php | 532 ++++ system/libraries/index.html | 11 + 286 files changed, 69842 insertions(+) create mode 100644 .htaccess create mode 100755 application/.htaccess create mode 100755 application/cache/index.html create mode 100755 application/config/autoload.php create mode 100755 application/config/config.php create mode 100755 application/config/constants.php create mode 100755 application/config/database.php create mode 100755 application/config/doctypes.php create mode 100755 application/config/foreign_chars.php create mode 100755 application/config/hooks.php create mode 100755 application/config/index.html create mode 100755 application/config/memcached.php create mode 100755 application/config/migration.php create mode 100755 application/config/mimes.php create mode 100755 application/config/profiler.php create mode 100755 application/config/routes.php create mode 100755 application/config/smileys.php create mode 100755 application/config/user_agents.php create mode 100644 application/controllers/Categories.php create mode 100644 application/controllers/Comments.php create mode 100644 application/controllers/Pages.php create mode 100644 application/controllers/Posts.php create mode 100644 application/controllers/Users.php create mode 100755 application/controllers/Welcome.php create mode 100755 application/controllers/index.html create mode 100755 application/core/index.html create mode 100755 application/helpers/index.html create mode 100755 application/hooks/index.html create mode 100755 application/index.html create mode 100755 application/language/english/index.html create mode 100755 application/language/index.html create mode 100755 application/libraries/index.html create mode 100755 application/logs/index.html create mode 100644 application/models/Category_model.php create mode 100644 application/models/Comment_model.php create mode 100644 application/models/Post_model.php create mode 100644 application/models/User_model.php create mode 100755 application/models/index.html create mode 100755 application/third_party/index.html create mode 100644 application/views/categories/create.php create mode 100644 application/views/categories/index.php create mode 100755 application/views/errors/cli/error_404.php create mode 100755 application/views/errors/cli/error_db.php create mode 100755 application/views/errors/cli/error_exception.php create mode 100755 application/views/errors/cli/error_general.php create mode 100755 application/views/errors/cli/error_php.php create mode 100755 application/views/errors/cli/index.html create mode 100755 application/views/errors/html/error_404.php create mode 100755 application/views/errors/html/error_db.php create mode 100755 application/views/errors/html/error_exception.php create mode 100755 application/views/errors/html/error_general.php create mode 100755 application/views/errors/html/error_php.php create mode 100755 application/views/errors/html/index.html create mode 100755 application/views/errors/index.html create mode 100755 application/views/index.html create mode 100644 application/views/pages/about.php create mode 100644 application/views/pages/home.php create mode 100644 application/views/posts/create.php create mode 100644 application/views/posts/edit.php create mode 100644 application/views/posts/index.php create mode 100644 application/views/posts/view.php create mode 100644 application/views/templates/footer.php create mode 100644 application/views/templates/header.php create mode 100644 application/views/users/login.php create mode 100644 application/views/users/register.php create mode 100755 application/views/welcome_message.php create mode 100644 assets/css/bootstrap.min.css create mode 100755 assets/css/style.css create mode 100644 assets/images/posts/OS_Ubuntu.png create mode 100644 assets/images/posts/angular.png create mode 100644 assets/images/posts/bibimbap.png create mode 100644 assets/images/posts/bitcoin.png create mode 100644 assets/images/posts/bitcoin1.png create mode 100644 assets/images/posts/ci.png create mode 100644 assets/images/posts/codeigniter.jpg create mode 100644 assets/images/posts/download.jpeg create mode 100644 assets/images/posts/noimage.png create mode 100644 assets/images/posts/noimage1.png create mode 100644 assets/images/posts/united-states.png create mode 100644 composer.json create mode 100644 contributing.md create mode 100644 index.php create mode 100644 license.txt create mode 100644 readme.rst create mode 100644 system/.htaccess create mode 100644 system/core/Benchmark.php create mode 100644 system/core/CodeIgniter.php create mode 100644 system/core/Common.php create mode 100644 system/core/Config.php create mode 100644 system/core/Controller.php create mode 100644 system/core/Exceptions.php create mode 100644 system/core/Hooks.php create mode 100644 system/core/Input.php create mode 100644 system/core/Lang.php create mode 100644 system/core/Loader.php create mode 100644 system/core/Log.php create mode 100644 system/core/Model.php create mode 100644 system/core/Output.php create mode 100644 system/core/Router.php create mode 100644 system/core/Security.php create mode 100644 system/core/URI.php create mode 100644 system/core/Utf8.php create mode 100644 system/core/compat/hash.php create mode 100644 system/core/compat/index.html create mode 100644 system/core/compat/mbstring.php create mode 100644 system/core/compat/password.php create mode 100644 system/core/compat/standard.php create mode 100644 system/core/index.html create mode 100644 system/database/DB.php create mode 100644 system/database/DB_cache.php create mode 100644 system/database/DB_driver.php create mode 100644 system/database/DB_forge.php create mode 100644 system/database/DB_query_builder.php create mode 100644 system/database/DB_result.php create mode 100644 system/database/DB_utility.php create mode 100644 system/database/drivers/cubrid/cubrid_driver.php create mode 100644 system/database/drivers/cubrid/cubrid_forge.php create mode 100644 system/database/drivers/cubrid/cubrid_result.php create mode 100644 system/database/drivers/cubrid/cubrid_utility.php create mode 100644 system/database/drivers/cubrid/index.html create mode 100644 system/database/drivers/ibase/ibase_driver.php create mode 100644 system/database/drivers/ibase/ibase_forge.php create mode 100644 system/database/drivers/ibase/ibase_result.php create mode 100644 system/database/drivers/ibase/ibase_utility.php create mode 100644 system/database/drivers/ibase/index.html create mode 100644 system/database/drivers/index.html create mode 100644 system/database/drivers/mssql/index.html create mode 100644 system/database/drivers/mssql/mssql_driver.php create mode 100644 system/database/drivers/mssql/mssql_forge.php create mode 100644 system/database/drivers/mssql/mssql_result.php create mode 100644 system/database/drivers/mssql/mssql_utility.php create mode 100644 system/database/drivers/mysql/index.html create mode 100644 system/database/drivers/mysql/mysql_driver.php create mode 100644 system/database/drivers/mysql/mysql_forge.php create mode 100644 system/database/drivers/mysql/mysql_result.php create mode 100644 system/database/drivers/mysql/mysql_utility.php create mode 100644 system/database/drivers/mysqli/index.html create mode 100644 system/database/drivers/mysqli/mysqli_driver.php create mode 100644 system/database/drivers/mysqli/mysqli_forge.php create mode 100644 system/database/drivers/mysqli/mysqli_result.php create mode 100644 system/database/drivers/mysqli/mysqli_utility.php create mode 100644 system/database/drivers/oci8/index.html create mode 100644 system/database/drivers/oci8/oci8_driver.php create mode 100644 system/database/drivers/oci8/oci8_forge.php create mode 100644 system/database/drivers/oci8/oci8_result.php create mode 100644 system/database/drivers/oci8/oci8_utility.php create mode 100644 system/database/drivers/odbc/index.html create mode 100644 system/database/drivers/odbc/odbc_driver.php create mode 100644 system/database/drivers/odbc/odbc_forge.php create mode 100644 system/database/drivers/odbc/odbc_result.php create mode 100644 system/database/drivers/odbc/odbc_utility.php create mode 100644 system/database/drivers/pdo/index.html create mode 100644 system/database/drivers/pdo/pdo_driver.php create mode 100644 system/database/drivers/pdo/pdo_forge.php create mode 100644 system/database/drivers/pdo/pdo_result.php create mode 100644 system/database/drivers/pdo/pdo_utility.php create mode 100644 system/database/drivers/pdo/subdrivers/index.html create mode 100644 system/database/drivers/pdo/subdrivers/pdo_4d_driver.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_4d_forge.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_cubrid_driver.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_cubrid_forge.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_dblib_driver.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_dblib_forge.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_firebird_driver.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_firebird_forge.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_ibm_driver.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_ibm_forge.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_informix_driver.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_informix_forge.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_mysql_driver.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_mysql_forge.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_oci_driver.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_oci_forge.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_odbc_driver.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_odbc_forge.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_pgsql_driver.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_pgsql_forge.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_sqlite_driver.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_sqlite_forge.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_sqlsrv_driver.php create mode 100644 system/database/drivers/pdo/subdrivers/pdo_sqlsrv_forge.php create mode 100644 system/database/drivers/postgre/index.html create mode 100644 system/database/drivers/postgre/postgre_driver.php create mode 100644 system/database/drivers/postgre/postgre_forge.php create mode 100644 system/database/drivers/postgre/postgre_result.php create mode 100644 system/database/drivers/postgre/postgre_utility.php create mode 100644 system/database/drivers/sqlite/index.html create mode 100644 system/database/drivers/sqlite/sqlite_driver.php create mode 100644 system/database/drivers/sqlite/sqlite_forge.php create mode 100644 system/database/drivers/sqlite/sqlite_result.php create mode 100644 system/database/drivers/sqlite/sqlite_utility.php create mode 100644 system/database/drivers/sqlite3/index.html create mode 100644 system/database/drivers/sqlite3/sqlite3_driver.php create mode 100644 system/database/drivers/sqlite3/sqlite3_forge.php create mode 100644 system/database/drivers/sqlite3/sqlite3_result.php create mode 100644 system/database/drivers/sqlite3/sqlite3_utility.php create mode 100644 system/database/drivers/sqlsrv/index.html create mode 100644 system/database/drivers/sqlsrv/sqlsrv_driver.php create mode 100644 system/database/drivers/sqlsrv/sqlsrv_forge.php create mode 100644 system/database/drivers/sqlsrv/sqlsrv_result.php create mode 100644 system/database/drivers/sqlsrv/sqlsrv_utility.php create mode 100644 system/database/index.html create mode 100644 system/fonts/index.html create mode 100644 system/fonts/texb.ttf create mode 100644 system/helpers/array_helper.php create mode 100644 system/helpers/captcha_helper.php create mode 100644 system/helpers/cookie_helper.php create mode 100644 system/helpers/date_helper.php create mode 100644 system/helpers/directory_helper.php create mode 100644 system/helpers/download_helper.php create mode 100644 system/helpers/email_helper.php create mode 100644 system/helpers/file_helper.php create mode 100644 system/helpers/form_helper.php create mode 100644 system/helpers/html_helper.php create mode 100644 system/helpers/index.html create mode 100644 system/helpers/inflector_helper.php create mode 100644 system/helpers/language_helper.php create mode 100644 system/helpers/number_helper.php create mode 100644 system/helpers/path_helper.php create mode 100644 system/helpers/security_helper.php create mode 100644 system/helpers/smiley_helper.php create mode 100644 system/helpers/string_helper.php create mode 100644 system/helpers/text_helper.php create mode 100644 system/helpers/typography_helper.php create mode 100644 system/helpers/url_helper.php create mode 100644 system/helpers/xml_helper.php create mode 100644 system/index.html create mode 100644 system/language/english/calendar_lang.php create mode 100644 system/language/english/date_lang.php create mode 100644 system/language/english/db_lang.php create mode 100644 system/language/english/email_lang.php create mode 100644 system/language/english/form_validation_lang.php create mode 100644 system/language/english/ftp_lang.php create mode 100644 system/language/english/imglib_lang.php create mode 100644 system/language/english/index.html create mode 100644 system/language/english/migration_lang.php create mode 100644 system/language/english/number_lang.php create mode 100644 system/language/english/pagination_lang.php create mode 100644 system/language/english/profiler_lang.php create mode 100644 system/language/english/unit_test_lang.php create mode 100644 system/language/english/upload_lang.php create mode 100644 system/language/index.html create mode 100644 system/libraries/Cache/Cache.php create mode 100644 system/libraries/Cache/drivers/Cache_apc.php create mode 100644 system/libraries/Cache/drivers/Cache_dummy.php create mode 100644 system/libraries/Cache/drivers/Cache_file.php create mode 100644 system/libraries/Cache/drivers/Cache_memcached.php create mode 100644 system/libraries/Cache/drivers/Cache_redis.php create mode 100644 system/libraries/Cache/drivers/Cache_wincache.php create mode 100644 system/libraries/Cache/drivers/index.html create mode 100644 system/libraries/Cache/index.html create mode 100644 system/libraries/Calendar.php create mode 100644 system/libraries/Cart.php create mode 100644 system/libraries/Driver.php create mode 100644 system/libraries/Email.php create mode 100644 system/libraries/Encrypt.php create mode 100644 system/libraries/Encryption.php create mode 100644 system/libraries/Form_validation.php create mode 100644 system/libraries/Ftp.php create mode 100644 system/libraries/Image_lib.php create mode 100644 system/libraries/Javascript.php create mode 100644 system/libraries/Javascript/Jquery.php create mode 100644 system/libraries/Javascript/index.html create mode 100644 system/libraries/Migration.php create mode 100644 system/libraries/Pagination.php create mode 100644 system/libraries/Parser.php create mode 100644 system/libraries/Profiler.php create mode 100644 system/libraries/Session/Session.php create mode 100644 system/libraries/Session/SessionHandlerInterface.php create mode 100644 system/libraries/Session/Session_driver.php create mode 100644 system/libraries/Session/drivers/Session_database_driver.php create mode 100644 system/libraries/Session/drivers/Session_files_driver.php create mode 100644 system/libraries/Session/drivers/Session_memcached_driver.php create mode 100644 system/libraries/Session/drivers/Session_redis_driver.php create mode 100644 system/libraries/Session/drivers/index.html create mode 100644 system/libraries/Session/index.html create mode 100644 system/libraries/Table.php create mode 100644 system/libraries/Trackback.php create mode 100644 system/libraries/Typography.php create mode 100644 system/libraries/Unit_test.php create mode 100644 system/libraries/Upload.php create mode 100644 system/libraries/User_agent.php create mode 100644 system/libraries/Xmlrpc.php create mode 100644 system/libraries/Xmlrpcs.php create mode 100644 system/libraries/Zip.php create mode 100644 system/libraries/index.html diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..7fcc900 --- /dev/null +++ b/.htaccess @@ -0,0 +1,5 @@ +RewriteEngine on +RewriteCond $1 !^(index.php|assets|images|js|css|uploads|favicon.png) +RewriteCond %(REQUEST_FILENAME) !-f +RewriteCond %(REQUEST_FILENAME) !-d +RewriteRule ^(.*)$ ./index.php/$1 [L] \ No newline at end of file diff --git a/application/.htaccess b/application/.htaccess new file mode 100755 index 0000000..6c63ed4 --- /dev/null +++ b/application/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + \ No newline at end of file diff --git a/application/cache/index.html b/application/cache/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/cache/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/config/autoload.php b/application/config/autoload.php new file mode 100755 index 0000000..7188f3b --- /dev/null +++ b/application/config/autoload.php @@ -0,0 +1,135 @@ + 'ua'); +*/ +$autoload['libraries'] = array('database','form_validation','session','pagination'); + +/* +| ------------------------------------------------------------------- +| Auto-load Drivers +| ------------------------------------------------------------------- +| These classes are located in system/libraries/ or in your +| application/libraries/ directory, but are also placed inside their +| own subdirectory and they extend the CI_Driver_Library class. They +| offer multiple interchangeable driver options. +| +| Prototype: +| +| $autoload['drivers'] = array('cache'); +| +| You can also supply an alternative property name to be assigned in +| the controller: +| +| $autoload['drivers'] = array('cache' => 'cch'); +| +*/ +$autoload['drivers'] = array(); + +/* +| ------------------------------------------------------------------- +| Auto-load Helper Files +| ------------------------------------------------------------------- +| Prototype: +| +| $autoload['helper'] = array('url', 'file'); +*/ +$autoload['helper'] = array('url','form','text','email'); + +/* +| ------------------------------------------------------------------- +| Auto-load Config files +| ------------------------------------------------------------------- +| Prototype: +| +| $autoload['config'] = array('config1', 'config2'); +| +| NOTE: This item is intended for use ONLY if you have created custom +| config files. Otherwise, leave it blank. +| +*/ +$autoload['config'] = array(); + +/* +| ------------------------------------------------------------------- +| Auto-load Language files +| ------------------------------------------------------------------- +| Prototype: +| +| $autoload['language'] = array('lang1', 'lang2'); +| +| NOTE: Do not include the "_lang" part of your file. For example +| "codeigniter_lang.php" would be referenced as array('codeigniter'); +| +*/ +$autoload['language'] = array(); + +/* +| ------------------------------------------------------------------- +| Auto-load Models +| ------------------------------------------------------------------- +| Prototype: +| +| $autoload['model'] = array('first_model', 'second_model'); +| +| You can also supply an alternative model name to be assigned +| in the controller: +| +| $autoload['model'] = array('first_model' => 'first'); +*/ +$autoload['model'] = array('post_model','category_model','comment_model','user_model'); diff --git a/application/config/config.php b/application/config/config.php new file mode 100755 index 0000000..6d07252 --- /dev/null +++ b/application/config/config.php @@ -0,0 +1,532 @@ +]+$/i +| +| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!! +| +*/ +$config['permitted_uri_chars'] = 'a-z 0-9~%.:_\-'; + +/* +|-------------------------------------------------------------------------- +| Enable Query Strings +|-------------------------------------------------------------------------- +| +| By default CodeIgniter uses search-engine friendly segment based URLs: +| example.com/who/what/where/ +| +| You can optionally enable standard query string based URLs: +| example.com?who=me&what=something&where=here +| +| Options are: TRUE or FALSE (boolean) +| +| The other items let you set the query string 'words' that will +| invoke your controllers and its functions: +| example.com/index.php?c=controller&m=function +| +| Please note that some of the helpers won't work as expected when +| this feature is enabled, since CodeIgniter is designed primarily to +| use segment based URLs. +| +*/ +$config['enable_query_strings'] = FALSE; +$config['controller_trigger'] = 'c'; +$config['function_trigger'] = 'm'; +$config['directory_trigger'] = 'd'; + +/* +|-------------------------------------------------------------------------- +| Allow $_GET array +|-------------------------------------------------------------------------- +| +| By default CodeIgniter enables access to the $_GET array. If for some +| reason you would like to disable it, set 'allow_get_array' to FALSE. +| +| WARNING: This feature is DEPRECATED and currently available only +| for backwards compatibility purposes! +| +*/ +$config['allow_get_array'] = TRUE; + +/* +|-------------------------------------------------------------------------- +| Error Logging Threshold +|-------------------------------------------------------------------------- +| +| You can enable error logging by setting a threshold over zero. The +| threshold determines what gets logged. Threshold options are: +| +| 0 = Disables logging, Error logging TURNED OFF +| 1 = Error Messages (including PHP errors) +| 2 = Debug Messages +| 3 = Informational Messages +| 4 = All Messages +| +| You can also pass an array with threshold levels to show individual error types +| +| array(2) = Debug Messages, without Error Messages +| +| For a live site you'll usually only enable Errors (1) to be logged otherwise +| your log files will fill up very fast. +| +*/ +$config['log_threshold'] = 0; + +/* +|-------------------------------------------------------------------------- +| Error Logging Directory Path +|-------------------------------------------------------------------------- +| +| Leave this BLANK unless you would like to set something other than the default +| application/logs/ directory. Use a full server path with trailing slash. +| +*/ +$config['log_path'] = ''; + +/* +|-------------------------------------------------------------------------- +| Log File Extension +|-------------------------------------------------------------------------- +| +| The default filename extension for log files. The default 'php' allows for +| protecting the log files via basic scripting, when they are to be stored +| under a publicly accessible directory. +| +| Note: Leaving it blank will default to 'php'. +| +*/ +$config['log_file_extension'] = ''; + +/* +|-------------------------------------------------------------------------- +| Log File Permissions +|-------------------------------------------------------------------------- +| +| The file system permissions to be applied on newly created log files. +| +| IMPORTANT: This MUST be an integer (no quotes) and you MUST use octal +| integer notation (i.e. 0700, 0644, etc.) +*/ +$config['log_file_permissions'] = 0644; + +/* +|-------------------------------------------------------------------------- +| Date Format for Logs +|-------------------------------------------------------------------------- +| +| Each item that is logged has an associated date. You can use PHP date +| codes to set your own date formatting +| +*/ +$config['log_date_format'] = 'Y-m-d H:i:s'; + +/* +|-------------------------------------------------------------------------- +| Error Views Directory Path +|-------------------------------------------------------------------------- +| +| Leave this BLANK unless you would like to set something other than the default +| application/views/errors/ directory. Use a full server path with trailing slash. +| +*/ +$config['error_views_path'] = ''; + +/* +|-------------------------------------------------------------------------- +| Cache Directory Path +|-------------------------------------------------------------------------- +| +| Leave this BLANK unless you would like to set something other than the default +| application/cache/ directory. Use a full server path with trailing slash. +| +*/ +$config['cache_path'] = ''; + +/* +|-------------------------------------------------------------------------- +| Cache Include Query String +|-------------------------------------------------------------------------- +| +| Whether to take the URL query string into consideration when generating +| output cache files. Valid options are: +| +| FALSE = Disabled +| TRUE = Enabled, take all query parameters into account. +| Please be aware that this may result in numerous cache +| files generated for the same page over and over again. +| array('q') = Enabled, but only take into account the specified list +| of query parameters. +| +*/ +$config['cache_query_string'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| Encryption Key +|-------------------------------------------------------------------------- +| +| If you use the Encryption class, you must set an encryption key. +| See the user guide for more info. +| +| https://codeigniter.com/userguide3/libraries/encryption.html +| +*/ +$config['encryption_key'] = ''; + +/* +|-------------------------------------------------------------------------- +| Session Variables +|-------------------------------------------------------------------------- +| +| 'sess_driver' +| +| The storage driver to use: files, database, redis, memcached +| +| 'sess_cookie_name' +| +| The session cookie name, must contain only [0-9a-z_-] characters +| +| 'sess_samesite' +| +| Session cookie SameSite attribute: Lax (default), Strict or None +| +| 'sess_expiration' +| +| The number of SECONDS you want the session to last. +| Setting to 0 (zero) means expire when the browser is closed. +| +| 'sess_save_path' +| +| The location to save sessions to, driver dependent. +| +| For the 'files' driver, it's a path to a writable directory. +| WARNING: Only absolute paths are supported! +| +| For the 'database' driver, it's a table name. +| Please read up the manual for the format with other session drivers. +| +| IMPORTANT: You are REQUIRED to set a valid save path! +| +| 'sess_match_ip' +| +| Whether to match the user's IP address when reading the session data. +| +| WARNING: If you're using the database driver, don't forget to update +| your session table's PRIMARY KEY when changing this setting. +| +| 'sess_time_to_update' +| +| How many seconds between CI regenerating the session ID. +| +| 'sess_regenerate_destroy' +| +| Whether to destroy session data associated with the old session ID +| when auto-regenerating the session ID. When set to FALSE, the data +| will be later deleted by the garbage collector. +| +| Other session cookie settings are shared with the rest of the application, +| except for 'cookie_prefix' and 'cookie_httponly', which are ignored here. +| +*/ +$config['sess_driver'] = 'files'; +$config['sess_cookie_name'] = 'ci_session'; +$config['sess_samesite'] = 'Lax'; +$config['sess_expiration'] = 7200; +$config['sess_save_path'] = NULL; +$config['sess_match_ip'] = FALSE; +$config['sess_time_to_update'] = 300; +$config['sess_regenerate_destroy'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| Cookie Related Variables +|-------------------------------------------------------------------------- +| +| 'cookie_prefix' = Set a cookie name prefix if you need to avoid collisions +| 'cookie_domain' = Set to .your-domain.com for site-wide cookies +| 'cookie_path' = Typically will be a forward slash +| 'cookie_secure' = Cookie will only be set if a secure HTTPS connection exists. +| 'cookie_httponly' = Cookie will only be accessible via HTTP(S) (no javascript) +| 'cookie_samesite' = Cookie's samesite attribute (Lax, Strict or None) +| +| Note: These settings (with the exception of 'cookie_prefix' and +| 'cookie_httponly') will also affect sessions. +| +*/ +$config['cookie_prefix'] = ''; +$config['cookie_domain'] = ''; +$config['cookie_path'] = '/'; +$config['cookie_secure'] = FALSE; +$config['cookie_httponly'] = FALSE; +$config['cookie_samesite'] = 'Lax'; + +/* +|-------------------------------------------------------------------------- +| Standardize newlines +|-------------------------------------------------------------------------- +| +| Determines whether to standardize newline characters in input data, +| meaning to replace \r\n, \r, \n occurrences with the PHP_EOL value. +| +| WARNING: This feature is DEPRECATED and currently available only +| for backwards compatibility purposes! +| +*/ +$config['standardize_newlines'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| Global XSS Filtering +|-------------------------------------------------------------------------- +| +| Determines whether the XSS filter is always active when GET, POST or +| COOKIE data is encountered +| +| WARNING: This feature is DEPRECATED and currently available only +| for backwards compatibility purposes! +| +*/ +$config['global_xss_filtering'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| Cross Site Request Forgery +|-------------------------------------------------------------------------- +| Enables a CSRF cookie token to be set. When set to TRUE, token will be +| checked on a submitted form. If you are accepting user data, it is strongly +| recommended CSRF protection be enabled. +| +| 'csrf_token_name' = The token name +| 'csrf_cookie_name' = The cookie name +| 'csrf_expire' = The number in seconds the token should expire. +| 'csrf_regenerate' = Regenerate token on every submission +| 'csrf_exclude_uris' = Array of URIs which ignore CSRF checks +*/ +$config['csrf_protection'] = FALSE; +$config['csrf_token_name'] = 'csrf_test_name'; +$config['csrf_cookie_name'] = 'csrf_cookie_name'; +$config['csrf_expire'] = 7200; +$config['csrf_regenerate'] = TRUE; +$config['csrf_exclude_uris'] = array(); + +/* +|-------------------------------------------------------------------------- +| Output Compression +|-------------------------------------------------------------------------- +| +| Enables Gzip output compression for faster page loads. When enabled, +| the output class will test whether your server supports Gzip. +| Even if it does, however, not all browsers support compression +| so enable only if you are reasonably sure your visitors can handle it. +| +| Only used if zlib.output_compression is turned off in your php.ini. +| Please do not use it together with httpd-level output compression. +| +| VERY IMPORTANT: If you are getting a blank page when compression is enabled it +| means you are prematurely outputting something to your browser. It could +| even be a line of whitespace at the end of one of your scripts. For +| compression to work, nothing can be sent before the output buffer is called +| by the output class. Do not 'echo' any values with compression enabled. +| +*/ +$config['compress_output'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| Master Time Reference +|-------------------------------------------------------------------------- +| +| Options are 'local' or any PHP supported timezone. This preference tells +| the system whether to use your server's local time as the master 'now' +| reference, or convert it to the configured one timezone. See the 'date +| helper' page of the user guide for information regarding date handling. +| +*/ +$config['time_reference'] = 'local'; + +/* +|-------------------------------------------------------------------------- +| Rewrite PHP Short Tags +|-------------------------------------------------------------------------- +| +| If your PHP installation does not have short tag support enabled CI +| can rewrite the tags on-the-fly, enabling you to utilize that syntax +| in your view files. Options are TRUE or FALSE (boolean) +| +| Note: You need to have eval() enabled for this to work. +| +*/ +$config['rewrite_short_tags'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| Reverse Proxy IPs +|-------------------------------------------------------------------------- +| +| If your server is behind a reverse proxy, you must whitelist the proxy +| IP addresses from which CodeIgniter should trust headers such as +| HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP in order to properly identify +| the visitor's IP address. +| +| You can use both an array or a comma-separated list of proxy addresses, +| as well as specifying whole subnets. Here are a few examples: +| +| Comma-separated: '10.0.1.200,192.168.5.0/24' +| Array: array('10.0.1.200', '192.168.5.0/24') +*/ +$config['proxy_ips'] = ''; diff --git a/application/config/constants.php b/application/config/constants.php new file mode 100755 index 0000000..18d3b4b --- /dev/null +++ b/application/config/constants.php @@ -0,0 +1,85 @@ +db->last_query() and profiling of DB queries. +| When you run a query, with this setting set to TRUE (default), +| CodeIgniter will store the SQL statement for debugging purposes. +| However, this may cause high memory usage, especially if you run +| a lot of SQL queries ... disable this to avoid that problem. +| +| The $active_group variable lets you choose which connection group to +| make active. By default there is only one group (the 'default' group). +| +| The $query_builder variables lets you determine whether or not to load +| the query builder class. +*/ +$active_group = 'default'; +$query_builder = TRUE; + +$db['default'] = array( + 'dsn' => '', + 'hostname' => 'localhost', + 'username' => 'jostheta', + 'password' => 'Pa$$w0rd', + 'database' => 'ciblog', + 'dbdriver' => 'mysqli', + 'dbprefix' => '', + 'pconnect' => FALSE, + 'db_debug' => (ENVIRONMENT !== 'production'), + 'cache_on' => FALSE, + 'cachedir' => '', + 'char_set' => 'utf8', + 'dbcollat' => 'utf8_general_ci', + 'swap_pre' => '', + 'encrypt' => FALSE, + 'compress' => FALSE, + 'stricton' => FALSE, + 'failover' => array(), + 'save_queries' => TRUE +); diff --git a/application/config/doctypes.php b/application/config/doctypes.php new file mode 100755 index 0000000..59a7991 --- /dev/null +++ b/application/config/doctypes.php @@ -0,0 +1,24 @@ + '', + 'xhtml1-strict' => '', + 'xhtml1-trans' => '', + 'xhtml1-frame' => '', + 'xhtml-basic11' => '', + 'html5' => '', + 'html4-strict' => '', + 'html4-trans' => '', + 'html4-frame' => '', + 'mathml1' => '', + 'mathml2' => '', + 'svg10' => '', + 'svg11' => '', + 'svg11-basic' => '', + 'svg11-tiny' => '', + 'xhtml-math-svg-xh' => '', + 'xhtml-math-svg-sh' => '', + 'xhtml-rdfa-1' => '', + 'xhtml-rdfa-2' => '' +); diff --git a/application/config/foreign_chars.php b/application/config/foreign_chars.php new file mode 100755 index 0000000..0231f35 --- /dev/null +++ b/application/config/foreign_chars.php @@ -0,0 +1,114 @@ + 'ae', + '/ö|œ/' => 'oe', + '/ü/' => 'ue', + '/Ä/' => 'Ae', + '/Ü/' => 'Ue', + '/Ö/' => 'Oe', + '/À|Á|Â|Ã|Ä|Å|Ǻ|Ā|Ă|Ą|Ǎ|Α|Ά|Ả|Ạ|Ầ|Ẫ|Ẩ|Ậ|Ằ|Ắ|Ẵ|Ẳ|Ặ|А/' => 'A', + '/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª|α|ά|ả|ạ|ầ|ấ|ẫ|ẩ|ậ|ằ|ắ|ẵ|ẳ|ặ|а/' => 'a', + '/Б/' => 'B', + '/б/' => 'b', + '/Ç|Ć|Ĉ|Ċ|Č/' => 'C', + '/ç|ć|ĉ|ċ|č/' => 'c', + '/Д|Δ/' => 'D', + '/д|δ/' => 'd', + '/Ð|Ď|Đ/' => 'Dj', + '/ð|ď|đ/' => 'dj', + '/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě|Ε|Έ|Ẽ|Ẻ|Ẹ|Ề|Ế|Ễ|Ể|Ệ|Е|Э/' => 'E', + '/è|é|ê|ë|ē|ĕ|ė|ę|ě|έ|ε|ẽ|ẻ|ẹ|ề|ế|ễ|ể|ệ|е|э/' => 'e', + '/Ф/' => 'F', + '/ф/' => 'f', + '/Ĝ|Ğ|Ġ|Ģ|Γ|Г|Ґ/' => 'G', + '/ĝ|ğ|ġ|ģ|γ|г|ґ/' => 'g', + '/Ĥ|Ħ/' => 'H', + '/ĥ|ħ/' => 'h', + '/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|Η|Ή|Ί|Ι|Ϊ|Ỉ|Ị|И|Ы/' => 'I', + '/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|η|ή|ί|ι|ϊ|ỉ|ị|и|ы|ї/' => 'i', + '/Ĵ/' => 'J', + '/ĵ/' => 'j', + '/Θ/' => 'TH', + '/θ/' => 'th', + '/Ķ|Κ|К/' => 'K', + '/ķ|κ|к/' => 'k', + '/Ĺ|Ļ|Ľ|Ŀ|Ł|Λ|Л/' => 'L', + '/ĺ|ļ|ľ|ŀ|ł|λ|л/' => 'l', + '/М/' => 'M', + '/м/' => 'm', + '/Ñ|Ń|Ņ|Ň|Ν|Н/' => 'N', + '/ñ|ń|ņ|ň|ʼn|ν|н/' => 'n', + '/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ|Ο|Ό|Ω|Ώ|Ỏ|Ọ|Ồ|Ố|Ỗ|Ổ|Ộ|Ờ|Ớ|Ỡ|Ở|Ợ|О/' => 'O', + '/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º|ο|ό|ω|ώ|ỏ|ọ|ồ|ố|ỗ|ổ|ộ|ờ|ớ|ỡ|ở|ợ|о/' => 'o', + '/П/' => 'P', + '/п/' => 'p', + '/Ŕ|Ŗ|Ř|Ρ|Р/' => 'R', + '/ŕ|ŗ|ř|ρ|р/' => 'r', + '/Ś|Ŝ|Ş|Ș|Š|Σ|С/' => 'S', + '/ś|ŝ|ş|ș|š|ſ|σ|ς|с/' => 's', + '/Ț|Ţ|Ť|Ŧ|Τ|Т/' => 'T', + '/ț|ţ|ť|ŧ|τ|т/' => 't', + '/Þ|þ/' => 'th', + '/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|Ũ|Ủ|Ụ|Ừ|Ứ|Ữ|Ử|Ự|У/' => 'U', + '/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ|υ|ύ|ϋ|ủ|ụ|ừ|ứ|ữ|ử|ự|у/' => 'u', + '/Ƴ|Ɏ|Ỵ|Ẏ|Ӳ|Ӯ|Ў|Ý|Ÿ|Ŷ|Υ|Ύ|Ϋ|Ỳ|Ỹ|Ỷ|Ỵ|Й/' => 'Y', + '/ẙ|ʏ|ƴ|ɏ|ỵ|ẏ|ӳ|ӯ|ў|ý|ÿ|ŷ|ỳ|ỹ|ỷ|ỵ|й/' => 'y', + '/В/' => 'V', + '/в/' => 'v', + '/Ŵ/' => 'W', + '/ŵ/' => 'w', + '/Φ/' => 'F', + '/φ/' => 'f', + '/Χ/' => 'CH', + '/χ/' => 'ch', + '/Ź|Ż|Ž|Ζ|З/' => 'Z', + '/ź|ż|ž|ζ|з/' => 'z', + '/Æ|Ǽ/' => 'AE', + '/ß/' => 'ss', + '/IJ/' => 'IJ', + '/ij/' => 'ij', + '/Œ/' => 'OE', + '/ƒ/' => 'f', + '/Ξ/' => 'KS', + '/ξ/' => 'ks', + '/Π/' => 'P', + '/π/' => 'p', + '/Β/' => 'V', + '/β/' => 'v', + '/Μ/' => 'M', + '/μ/' => 'm', + '/Ψ/' => 'PS', + '/ψ/' => 'ps', + '/Ё/' => 'Yo', + '/ё/' => 'yo', + '/Є/' => 'Ye', + '/є/' => 'ye', + '/Ї/' => 'Yi', + '/Ж/' => 'Zh', + '/ж/' => 'zh', + '/Х/' => 'Kh', + '/х/' => 'kh', + '/Ц/' => 'Ts', + '/ц/' => 'ts', + '/Ч/' => 'Ch', + '/ч/' => 'ch', + '/Ш/' => 'Sh', + '/ш/' => 'sh', + '/Щ/' => 'Shch', + '/щ/' => 'shch', + '/Ъ|ъ|Ь|ь/' => '', + '/Ю/' => 'Yu', + '/ю/' => 'yu', + '/Я/' => 'Ya', + '/я/' => 'ya' +); diff --git a/application/config/hooks.php b/application/config/hooks.php new file mode 100755 index 0000000..79c5c16 --- /dev/null +++ b/application/config/hooks.php @@ -0,0 +1,13 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/config/memcached.php b/application/config/memcached.php new file mode 100755 index 0000000..65a1496 --- /dev/null +++ b/application/config/memcached.php @@ -0,0 +1,19 @@ + array( + 'hostname' => '127.0.0.1', + 'port' => '11211', + 'weight' => '1', + ), +); diff --git a/application/config/migration.php b/application/config/migration.php new file mode 100755 index 0000000..4b585a6 --- /dev/null +++ b/application/config/migration.php @@ -0,0 +1,84 @@ +migration->current() this is the version that schema will +| be upgraded / downgraded to. +| +*/ +$config['migration_version'] = 0; + +/* +|-------------------------------------------------------------------------- +| Migrations Path +|-------------------------------------------------------------------------- +| +| Path to your migrations folder. +| Typically, it will be within your application path. +| Also, writing permission is required within the migrations path. +| +*/ +$config['migration_path'] = APPPATH.'migrations/'; diff --git a/application/config/mimes.php b/application/config/mimes.php new file mode 100755 index 0000000..b2e989f --- /dev/null +++ b/application/config/mimes.php @@ -0,0 +1,186 @@ + array('application/mac-binhex40', 'application/mac-binhex', 'application/x-binhex40', 'application/x-mac-binhex40'), + 'cpt' => 'application/mac-compactpro', + 'csv' => array('text/x-comma-separated-values', 'text/comma-separated-values', 'application/octet-stream', 'application/vnd.ms-excel', 'application/x-csv', 'text/x-csv', 'text/csv', 'application/csv', 'application/excel', 'application/vnd.msexcel', 'text/plain'), + 'bin' => array('application/macbinary', 'application/mac-binary', 'application/octet-stream', 'application/x-binary', 'application/x-macbinary'), + 'dms' => 'application/octet-stream', + 'lha' => 'application/octet-stream', + 'lzh' => 'application/octet-stream', + 'exe' => array('application/octet-stream', 'application/x-msdownload'), + 'class' => 'application/octet-stream', + 'psd' => array('application/x-photoshop', 'image/vnd.adobe.photoshop'), + 'so' => 'application/octet-stream', + 'sea' => 'application/octet-stream', + 'dll' => 'application/octet-stream', + 'oda' => 'application/oda', + 'pdf' => array('application/pdf', 'application/force-download', 'application/x-download', 'binary/octet-stream'), + 'ai' => array('application/pdf', 'application/postscript'), + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'mif' => 'application/vnd.mif', + 'xls' => array('application/vnd.ms-excel', 'application/msexcel', 'application/x-msexcel', 'application/x-ms-excel', 'application/x-excel', 'application/x-dos_ms_excel', 'application/xls', 'application/x-xls', 'application/excel', 'application/download', 'application/vnd.ms-office', 'application/msword'), + 'ppt' => array('application/powerpoint', 'application/vnd.ms-powerpoint', 'application/vnd.ms-office', 'application/msword'), + 'pptx' => array('application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/x-zip', 'application/zip'), + 'wbxml' => 'application/wbxml', + 'wmlc' => 'application/wmlc', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dxr' => 'application/x-director', + 'dvi' => 'application/x-dvi', + 'gtar' => 'application/x-gtar', + 'gz' => 'application/x-gzip', + 'gzip' => 'application/x-gzip', + 'php' => array('application/x-httpd-php', 'application/php', 'application/x-php', 'text/php', 'text/x-php', 'application/x-httpd-php-source'), + 'php4' => 'application/x-httpd-php', + 'php3' => 'application/x-httpd-php', + 'phtml' => 'application/x-httpd-php', + 'phps' => 'application/x-httpd-php-source', + 'js' => array('application/x-javascript', 'text/plain'), + 'swf' => 'application/x-shockwave-flash', + 'sit' => 'application/x-stuffit', + 'tar' => 'application/x-tar', + 'tgz' => array('application/x-tar', 'application/x-gzip-compressed'), + 'z' => 'application/x-compress', + 'xhtml' => 'application/xhtml+xml', + 'xht' => 'application/xhtml+xml', + 'zip' => array('application/x-zip', 'application/zip', 'application/x-zip-compressed', 'application/s-compressed', 'multipart/x-zip'), + 'rar' => array('application/x-rar', 'application/rar', 'application/x-rar-compressed'), + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mpga' => 'audio/mpeg', + 'mp2' => 'audio/mpeg', + 'mp3' => array('audio/mpeg', 'audio/mpg', 'audio/mpeg3', 'audio/mp3'), + 'aif' => array('audio/x-aiff', 'audio/aiff'), + 'aiff' => array('audio/x-aiff', 'audio/aiff'), + 'aifc' => 'audio/x-aiff', + 'ram' => 'audio/x-pn-realaudio', + 'rm' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'ra' => 'audio/x-realaudio', + 'rv' => 'video/vnd.rn-realvideo', + 'wav' => array('audio/x-wav', 'audio/wave', 'audio/wav'), + 'bmp' => array('image/bmp', 'image/x-bmp', 'image/x-bitmap', 'image/x-xbitmap', 'image/x-win-bitmap', 'image/x-windows-bmp', 'image/ms-bmp', 'image/x-ms-bmp', 'application/bmp', 'application/x-bmp', 'application/x-win-bitmap'), + 'gif' => 'image/gif', + 'jpeg' => array('image/jpeg', 'image/pjpeg'), + 'jpg' => array('image/jpeg', 'image/pjpeg'), + 'jpe' => array('image/jpeg', 'image/pjpeg'), + 'jp2' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'j2k' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'jpf' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'jpg2' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'jpx' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'jpm' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'mj2' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'mjp2' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'png' => array('image/png', 'image/x-png'), + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'heic' => 'image/heic', + 'heif' => 'image/heif', + 'css' => array('text/css', 'text/plain'), + 'html' => array('text/html', 'text/plain'), + 'htm' => array('text/html', 'text/plain'), + 'shtml' => array('text/html', 'text/plain'), + 'txt' => 'text/plain', + 'text' => 'text/plain', + 'log' => array('text/plain', 'text/x-log'), + 'rtx' => 'text/richtext', + 'rtf' => 'text/rtf', + 'xml' => array('application/xml', 'text/xml', 'text/plain'), + 'xsl' => array('application/xml', 'text/xsl', 'text/xml'), + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpe' => 'video/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + 'avi' => array('video/x-msvideo', 'video/msvideo', 'video/avi', 'application/x-troff-msvideo'), + 'movie' => 'video/x-sgi-movie', + 'doc' => array('application/msword', 'application/vnd.ms-office'), + 'docx' => array('application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/zip', 'application/msword', 'application/x-zip'), + 'dot' => array('application/msword', 'application/vnd.ms-office'), + 'dotx' => array('application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/zip', 'application/msword'), + 'xlsx' => array('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/zip', 'application/vnd.ms-excel', 'application/msword', 'application/x-zip'), + 'word' => array('application/msword', 'application/octet-stream'), + 'xl' => 'application/excel', + 'eml' => 'message/rfc822', + 'json' => array('application/json', 'text/json'), + 'pem' => array('application/x-x509-user-cert', 'application/x-pem-file', 'application/octet-stream'), + 'p10' => array('application/x-pkcs10', 'application/pkcs10'), + 'p12' => 'application/x-pkcs12', + 'p7a' => 'application/x-pkcs7-signature', + 'p7c' => array('application/pkcs7-mime', 'application/x-pkcs7-mime'), + 'p7m' => array('application/pkcs7-mime', 'application/x-pkcs7-mime'), + 'p7r' => 'application/x-pkcs7-certreqresp', + 'p7s' => 'application/pkcs7-signature', + 'crt' => array('application/x-x509-ca-cert', 'application/x-x509-user-cert', 'application/pkix-cert'), + 'crl' => array('application/pkix-crl', 'application/pkcs-crl'), + 'der' => 'application/x-x509-ca-cert', + 'kdb' => 'application/octet-stream', + 'pgp' => 'application/pgp', + 'gpg' => 'application/gpg-keys', + 'sst' => 'application/octet-stream', + 'csr' => 'application/octet-stream', + 'rsa' => 'application/x-pkcs7', + 'cer' => array('application/pkix-cert', 'application/x-x509-ca-cert'), + '3g2' => 'video/3gpp2', + '3gp' => array('video/3gp', 'video/3gpp'), + 'mp4' => 'video/mp4', + 'm4a' => 'audio/x-m4a', + 'f4v' => array('video/mp4', 'video/x-f4v'), + 'flv' => 'video/x-flv', + 'webm' => 'video/webm', + 'aac' => array('audio/x-aac', 'audio/aac'), + 'm4u' => 'application/vnd.mpegurl', + 'm3u' => 'text/plain', + 'xspf' => 'application/xspf+xml', + 'vlc' => 'application/videolan', + 'wmv' => array('video/x-ms-wmv', 'video/x-ms-asf'), + 'au' => 'audio/x-au', + 'ac3' => 'audio/ac3', + 'flac' => 'audio/x-flac', + 'ogg' => array('audio/ogg', 'video/ogg', 'application/ogg'), + 'kmz' => array('application/vnd.google-earth.kmz', 'application/zip', 'application/x-zip'), + 'kml' => array('application/vnd.google-earth.kml+xml', 'application/xml', 'text/xml'), + 'ics' => 'text/calendar', + 'ical' => 'text/calendar', + 'zsh' => 'text/x-scriptzsh', + '7z' => array('application/x-7z-compressed', 'application/x-compressed', 'application/x-zip-compressed', 'application/zip', 'multipart/x-zip'), + '7zip' => array('application/x-7z-compressed', 'application/x-compressed', 'application/x-zip-compressed', 'application/zip', 'multipart/x-zip'), + 'cdr' => array('application/cdr', 'application/coreldraw', 'application/x-cdr', 'application/x-coreldraw', 'image/cdr', 'image/x-cdr', 'zz-application/zz-winassoc-cdr'), + 'wma' => array('audio/x-ms-wma', 'video/x-ms-asf'), + 'jar' => array('application/java-archive', 'application/x-java-application', 'application/x-jar', 'application/x-compressed'), + 'svg' => array('image/svg+xml', 'image/svg', 'application/xml', 'text/xml'), + 'vcf' => 'text/x-vcard', + 'srt' => array('text/srt', 'text/plain'), + 'vtt' => array('text/vtt', 'text/plain'), + 'ico' => array('image/x-icon', 'image/x-ico', 'image/vnd.microsoft.icon'), + 'odc' => 'application/vnd.oasis.opendocument.chart', + 'otc' => 'application/vnd.oasis.opendocument.chart-template', + 'odf' => 'application/vnd.oasis.opendocument.formula', + 'otf' => 'application/vnd.oasis.opendocument.formula-template', + 'odg' => 'application/vnd.oasis.opendocument.graphics', + 'otg' => 'application/vnd.oasis.opendocument.graphics-template', + 'odi' => 'application/vnd.oasis.opendocument.image', + 'oti' => 'application/vnd.oasis.opendocument.image-template', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'otp' => 'application/vnd.oasis.opendocument.presentation-template', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'odm' => 'application/vnd.oasis.opendocument.text-master', + 'ott' => 'application/vnd.oasis.opendocument.text-template', + 'oth' => 'application/vnd.oasis.opendocument.text-web' +); diff --git a/application/config/profiler.php b/application/config/profiler.php new file mode 100755 index 0000000..3436e93 --- /dev/null +++ b/application/config/profiler.php @@ -0,0 +1,14 @@ + array('grin.gif', '19', '19', 'grin'), + ':lol:' => array('lol.gif', '19', '19', 'LOL'), + ':cheese:' => array('cheese.gif', '19', '19', 'cheese'), + ':)' => array('smile.gif', '19', '19', 'smile'), + ';-)' => array('wink.gif', '19', '19', 'wink'), + ';)' => array('wink.gif', '19', '19', 'wink'), + ':smirk:' => array('smirk.gif', '19', '19', 'smirk'), + ':roll:' => array('rolleyes.gif', '19', '19', 'rolleyes'), + ':-S' => array('confused.gif', '19', '19', 'confused'), + ':wow:' => array('surprise.gif', '19', '19', 'surprised'), + ':bug:' => array('bigsurprise.gif', '19', '19', 'big surprise'), + ':-P' => array('tongue_laugh.gif', '19', '19', 'tongue laugh'), + '%-P' => array('tongue_rolleye.gif', '19', '19', 'tongue rolleye'), + ';-P' => array('tongue_wink.gif', '19', '19', 'tongue wink'), + ':P' => array('raspberry.gif', '19', '19', 'raspberry'), + ':blank:' => array('blank.gif', '19', '19', 'blank stare'), + ':long:' => array('longface.gif', '19', '19', 'long face'), + ':ohh:' => array('ohh.gif', '19', '19', 'ohh'), + ':grrr:' => array('grrr.gif', '19', '19', 'grrr'), + ':gulp:' => array('gulp.gif', '19', '19', 'gulp'), + '8-/' => array('ohoh.gif', '19', '19', 'oh oh'), + ':down:' => array('downer.gif', '19', '19', 'downer'), + ':red:' => array('embarrassed.gif', '19', '19', 'red face'), + ':sick:' => array('sick.gif', '19', '19', 'sick'), + ':shut:' => array('shuteye.gif', '19', '19', 'shut eye'), + ':-/' => array('hmm.gif', '19', '19', 'hmmm'), + '>:(' => array('mad.gif', '19', '19', 'mad'), + ':mad:' => array('mad.gif', '19', '19', 'mad'), + '>:-(' => array('angry.gif', '19', '19', 'angry'), + ':angry:' => array('angry.gif', '19', '19', 'angry'), + ':zip:' => array('zip.gif', '19', '19', 'zipper'), + ':kiss:' => array('kiss.gif', '19', '19', 'kiss'), + ':ahhh:' => array('shock.gif', '19', '19', 'shock'), + ':coolsmile:' => array('shade_smile.gif', '19', '19', 'cool smile'), + ':coolsmirk:' => array('shade_smirk.gif', '19', '19', 'cool smirk'), + ':coolgrin:' => array('shade_grin.gif', '19', '19', 'cool grin'), + ':coolhmm:' => array('shade_hmm.gif', '19', '19', 'cool hmm'), + ':coolmad:' => array('shade_mad.gif', '19', '19', 'cool mad'), + ':coolcheese:' => array('shade_cheese.gif', '19', '19', 'cool cheese'), + ':vampire:' => array('vampire.gif', '19', '19', 'vampire'), + ':snake:' => array('snake.gif', '19', '19', 'snake'), + ':exclaim:' => array('exclaim.gif', '19', '19', 'exclaim'), + ':question:' => array('question.gif', '19', '19', 'question') + +); diff --git a/application/config/user_agents.php b/application/config/user_agents.php new file mode 100755 index 0000000..5e1f6af --- /dev/null +++ b/application/config/user_agents.php @@ -0,0 +1,222 @@ + 'Windows 10', + 'windows nt 6.3' => 'Windows 8.1', + 'windows nt 6.2' => 'Windows 8', + '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 4.0', + 'winnt4.0' => 'Windows NT 4.0', + 'winnt 4.0' => 'Windows NT', + 'winnt' => 'Windows NT', + 'windows 98' => 'Windows 98', + 'win98' => 'Windows 98', + 'windows 95' => 'Windows 95', + 'win95' => 'Windows 95', + 'windows phone' => 'Windows Phone', + 'windows' => 'Unknown Windows OS', + 'android' => 'Android', + 'blackberry' => 'BlackBerry', + 'iphone' => 'iOS', + 'ipad' => 'iOS', + 'ipod' => 'iOS', + 'os x' => 'Mac OS X', + 'ppc mac' => 'Power PC Mac', + 'freebsd' => 'FreeBSD', + 'ppc' => 'Macintosh', + 'linux' => 'Linux', + 'debian' => 'Debian', + 'sunos' => 'Sun Solaris', + 'beos' => 'BeOS', + 'apachebench' => 'ApacheBench', + 'aix' => 'AIX', + 'irix' => 'Irix', + 'osf' => 'DEC OSF', + 'hp-ux' => 'HP-UX', + 'netbsd' => 'NetBSD', + 'bsdi' => 'BSDi', + 'openbsd' => 'OpenBSD', + 'gnu' => 'GNU/Linux', + 'unix' => 'Unknown Unix OS', + 'symbian' => 'Symbian OS' +); + + +// The order of this array should NOT be changed. Many browsers return +// multiple browser types so we want to identify the sub-type first. +$browsers = array( + 'OPR' => 'Opera', + 'Flock' => 'Flock', + 'Edge' => 'Edge', + 'Chrome' => 'Chrome', + // Opera 10+ always reports Opera/9.80 and appends Version/ to the user agent string + 'Opera.*?Version' => 'Opera', + 'Opera' => 'Opera', + 'MSIE' => 'Internet Explorer', + 'Internet Explorer' => 'Internet Explorer', + 'Trident.* rv' => 'Internet Explorer', + 'Shiira' => 'Shiira', + 'Firefox' => 'Firefox', + 'Chimera' => 'Chimera', + 'Phoenix' => 'Phoenix', + 'Firebird' => 'Firebird', + 'Camino' => 'Camino', + 'Netscape' => 'Netscape', + 'OmniWeb' => 'OmniWeb', + 'Safari' => 'Safari', + 'Mozilla' => 'Mozilla', + 'Konqueror' => 'Konqueror', + 'icab' => 'iCab', + 'Lynx' => 'Lynx', + 'Links' => 'Links', + 'hotjava' => 'HotJava', + 'amaya' => 'Amaya', + 'IBrowse' => 'IBrowse', + 'Maxthon' => 'Maxthon', + 'Ubuntu' => 'Ubuntu Web Browser' +); + +$mobiles = array( + // legacy array, old values commented out + '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' + + // Phones and Manufacturers + 'motorola' => 'Motorola', + 'nokia' => 'Nokia', + 'nexus' => 'Nexus', + 'palm' => 'Palm', + 'iphone' => 'Apple iPhone', + 'ipad' => 'iPad', + 'ipod' => 'Apple iPod Touch', + 'sony' => 'Sony Ericsson', + 'ericsson' => 'Sony Ericsson', + 'blackberry' => 'BlackBerry', + 'cocoon' => 'O2 Cocoon', + 'blazer' => 'Treo', + 'lg' => 'LG', + 'amoi' => 'Amoi', + 'xda' => 'XDA', + 'mda' => 'MDA', + 'vario' => 'Vario', + 'htc' => 'HTC', + 'samsung' => 'Samsung', + 'sharp' => 'Sharp', + 'sie-' => 'Siemens', + 'alcatel' => 'Alcatel', + 'benq' => 'BenQ', + 'ipaq' => 'HP iPaq', + 'mot-' => 'Motorola', + 'playstation portable' => 'PlayStation Portable', + 'playstation 3' => 'PlayStation 3', + 'playstation vita' => 'PlayStation Vita', + 'hiptop' => 'Danger Hiptop', + 'nec-' => 'NEC', + 'panasonic' => 'Panasonic', + 'philips' => 'Philips', + 'sagem' => 'Sagem', + 'sanyo' => 'Sanyo', + 'spv' => 'SPV', + 'zte' => 'ZTE', + 'sendo' => 'Sendo', + 'nintendo dsi' => 'Nintendo DSi', + 'nintendo ds' => 'Nintendo DS', + 'nintendo 3ds' => 'Nintendo 3DS', + 'wii' => 'Nintendo Wii', + 'open web' => 'Open Web', + 'openweb' => 'OpenWeb', + 'meizu' => 'Meizu', + 'huawei' => 'Huawei', + 'xiaomi' => 'Xiaomi', + 'oppo' => 'Oppo', + 'vivo' => 'Vivo', + 'infinix' => 'Infinix', + + // Operating Systems + 'android' => 'Android', + 'symbian' => 'Symbian', + 'SymbianOS' => 'SymbianOS', + 'elaine' => 'Palm', + 'series60' => 'Symbian S60', + 'windows ce' => 'Windows CE', + + // Browsers + 'obigo' => 'Obigo', + 'netfront' => 'Netfront Browser', + 'openwave' => 'Openwave Browser', + 'mobilexplorer' => 'Mobile Explorer', + 'operamini' => 'Opera Mini', + 'opera mini' => 'Opera Mini', + 'opera mobi' => 'Opera Mobile', + 'fennec' => 'Firefox Mobile', + + // Other + 'digital paths' => 'Digital Paths', + 'avantgo' => 'AvantGo', + 'xiino' => 'Xiino', + 'novarra' => 'Novarra Transcoder', + 'vodafone' => 'Vodafone', + 'docomo' => 'NTT DoCoMo', + 'o2' => 'O2', + + // Fallback + 'mobile' => 'Generic Mobile', + 'wireless' => 'Generic Mobile', + 'j2me' => 'Generic Mobile', + 'midp' => 'Generic Mobile', + 'cldc' => 'Generic Mobile', + 'up.link' => 'Generic Mobile', + 'up.browser' => 'Generic Mobile', + 'smartphone' => 'Generic Mobile', + 'cellphone' => 'Generic Mobile' +); + +// There are hundreds of bots but these are the most common. +$robots = array( + 'googlebot' => 'Googlebot', + 'msnbot' => 'MSNBot', + 'baiduspider' => 'Baiduspider', + 'bingbot' => 'Bing', + 'slurp' => 'Inktomi Slurp', + 'yahoo' => 'Yahoo', + 'ask jeeves' => 'Ask Jeeves', + 'fastcrawler' => 'FastCrawler', + 'infoseek' => 'InfoSeek Robot 1.0', + 'lycos' => 'Lycos', + 'yandex' => 'YandexBot', + 'mediapartners-google' => 'MediaPartners Google', + 'CRAZYWEBCRAWLER' => 'Crazy Webcrawler', + 'adsbot-google' => 'AdsBot Google', + 'feedfetcher-google' => 'Feedfetcher Google', + 'curious george' => 'Curious George', + 'ia_archiver' => 'Alexa Crawler', + 'MJ12bot' => 'Majestic-12', + 'Uptimebot' => 'Uptimebot', + 'UptimeRobot' => 'UptimeRobot' +); diff --git a/application/controllers/Categories.php b/application/controllers/Categories.php new file mode 100644 index 0000000..20fff72 --- /dev/null +++ b/application/controllers/Categories.php @@ -0,0 +1,68 @@ +category_model->get_categories(); + + $this->load->view('templates/header'); + $this->load->view('categories/index', $data); + $this->load->view('templates/footer'); + + } + //To create a category + public function create() + { + //check login + if(!$this->session->userdata('logged_in')){ + redirect('users/login'); + } + $data['title'] = 'Create category'; + $this->form_validation->set_rules('name','Name','required'); + + if($this->form_validation->run() === FALSE){ + $this->load->view('templates/header'); + $this->load->view('categories/create', $data); + $this->load->view('templates/footer'); + } + else + { + $this->category_model->create_category(); + //flash message + $this->session->set_flashdata('category_created','New category has been created'); + redirect('categories'); + } + } + + //to display the posts of each particular category + public function posts($id) + { + $data['title'] = $this->category_model->get_category($id)->name; + $data['posts'] = $this->post_model->get_posts_by_category($id); + + $this->load->view('templates/header'); + $this->load->view('posts/index', $data); + $this->load->view('templates/footer'); + } + + public function delete($id){ + // Check login + if(!$this->session->userdata('logged_in')){ + redirect('users/login'); + } + + $this->category_model->delete_category($id); + + // Set message + $this->session->set_flashdata('category_deleted', 'Your category has been deleted'); + + redirect('categories'); + } + + + } + + + \ No newline at end of file diff --git a/application/controllers/Comments.php b/application/controllers/Comments.php new file mode 100644 index 0000000..c7d8c44 --- /dev/null +++ b/application/controllers/Comments.php @@ -0,0 +1,28 @@ +input->post('slug'); + $data['post'] = $this->post_model->get_posts($slug); + + $this->form_validation->set_rules('name','Name','required'); + $this->form_validation->set_rules('email','Email','required'); + $this->form_validation->set_rules('email','Email','required|valid_email'); + $this->form_validation->set_rules('body','Body','required'); + + + if($this->form_validation->run() === FALSE) + { + $this->load->view('templates/header'); + $this->load->view('posts/view', $data); + $this->load->view('templates/footer'); + } + else + { + $this->comment_model->create_comment($post_id); + redirect('posts/'.$slug); + } + + + } + } \ No newline at end of file diff --git a/application/controllers/Pages.php b/application/controllers/Pages.php new file mode 100644 index 0000000..a05e0d2 --- /dev/null +++ b/application/controllers/Pages.php @@ -0,0 +1,22 @@ +load->view('templates/header'); + $this->load->view('pages/'.$page, $data); + $this->load->view('templates/footer'); + } + +} + + diff --git a/application/controllers/Posts.php b/application/controllers/Posts.php new file mode 100644 index 0000000..c16f16b --- /dev/null +++ b/application/controllers/Posts.php @@ -0,0 +1,145 @@ +db->count_all('posts'); + $config['per_page'] = 3; + $config['uri_segment'] = 3; + $config['attributes'] = array('class' => 'pagination-links'); + + // Init Pagination + $this->pagination->initialize($config); + + $data['title'] = 'Latest Posts'; + $data['posts'] = $this->post_model->get_posts(FALSE,$config['per_page'],$offset); + + $this->load->view('templates/header'); + $this->load->view('posts/index', $data); + $this->load->view('templates/footer'); + } + + //Displays each individual post + public function view($slug = NULL) + { + $data['post'] = $this->post_model->get_posts($slug); + $post_id = $data['post']['id']; + $data['comments'] = $this->comment_model->get_comments($post_id); + + if (empty($data['post'])) { + show_404(); + } + + $data['title'] = $data['post']['title']; + + $this->load->view('templates/header'); + $this->load->view('posts/view', $data); + $this->load->view('templates/footer'); + + } + + //Handles the create post + public function create(){ + //check login + if(!$this->session->userdata('logged_in')){ + redirect('users/login'); + } + + $data['title'] = 'Create Post'; + $data['categories'] = $this->post_model->get_categories(); + + //validations + $this->form_validation->set_rules('title','Title','required'); + $this->form_validation->set_rules('body','Body','required'); + + if($this->form_validation->run() === FALSE) + { + $this->load->view('templates/header'); + $this->load->view('posts/create', $data); + $this->load->view('templates/footer'); + } + else + { + //Upload Image + $config['upload_path'] = './assets/images/posts'; + $config['allowed_types'] = 'gif|jpeg|png'; + $config['max_size'] = '2048'; + $config['max_width'] = '2000'; + $config['max_height'] = '2000'; + + $this->load->library('upload', $config); + + if(!$this->upload->do_upload('postimage')) + { + $errors = array('error' => $this->upload->display_errors()); + $post_image = 'noimage.jpg'; + } + else + { + $data = array('upload_data' => $this->upload->data); + $post_image = $_FILES['postimage']['name']; + } + + $this->post_model->create_post($post_image); + //set flash messages + $this->session->set_flashdata('post_created','Your post has been created'); + redirect('posts'); + } + } + + //to delete the post + public function delete($id){ + //check login + if(!$this->session->userdata('logged_in')){ + redirect('users/login'); + } + + $this->post_model->delete_post($id); + $this->session->set_flashdata('post_deleted','Your post has been deleted'); + redirect('posts'); + } + + //function to edit posts + public function edit($slug){ + //check login + if(!$this->session->userdata('logged_in')){ + redirect('users/login'); + } + + $data['post'] = $this->post_model->get_posts($slug); + + //Check user + if($this->session->userdata('user_id') != $this->post_model->get_posts($slug)['user_id']){ + redirect('posts'); + } + $data['categories'] = $this->post_model->get_categories(); + + if (empty($data['post'])) { + show_404(); + } + + $data['title'] = 'Edit Post'; + + $this->load->view('templates/header'); + $this->load->view('posts/edit', $data); + $this->load->view('templates/footer'); + } + + //function to update posts + public function update(){ + //check login + if(!$this->session->userdata('logged_in')){ + redirect('users/login'); + } + + $this->post_model->update_post(); + $this->session->set_flashdata('post_updated','Your post has been updated'); + redirect('posts'); + } + +} + diff --git a/application/controllers/Users.php b/application/controllers/Users.php new file mode 100644 index 0000000..e03686b --- /dev/null +++ b/application/controllers/Users.php @@ -0,0 +1,106 @@ +form_validation->set_rules('name', 'Name', 'required'); + $this->form_validation->set_rules('username', 'Username', 'required|callback_check_username_exists'); + $this->form_validation->set_rules('email', 'Email', 'required|callback_check_email_exists'); + $this->form_validation->set_rules('password', 'Password', 'required'); + $this->form_validation->set_rules('password2', 'Confirm Password', 'matches[password]'); + + if($this->form_validation->run() === FALSE){ + $this->load->view('templates/header'); + $this->load->view('users/register', $data); + $this->load->view('templates/footer'); + } else { + // Encrypt password + $enc_password = md5($this->input->post('password')); + + $this->user_model->register($enc_password); + + // Set message + $this->session->set_flashdata('user_registered', 'You are now registered and can log in'); + + redirect('posts'); + } + } + + // Log in user + public function login(){ + $data['title'] = 'Sign In'; + + $this->form_validation->set_rules('username', 'Username', 'required'); + $this->form_validation->set_rules('password', 'Password', 'required'); + + if($this->form_validation->run() === FALSE){ + $this->load->view('templates/header'); + $this->load->view('users/login', $data); + $this->load->view('templates/footer'); + } else { + + // Get username + $username = $this->input->post('username'); + // Get and encrypt the password + $password = md5($this->input->post('password')); + + // Login user + $user_id = $this->user_model->login($username, $password); + + if($user_id){ + // Create session + $user_data = array( + 'user_id' => $user_id, + 'username' => $username, + 'logged_in' => true + ); + + $this->session->set_userdata($user_data); + + // Set message + $this->session->set_flashdata('user_loggedin', 'You are now logged in'); + + redirect('posts'); + } else { + // Set message + $this->session->set_flashdata('login_failed', 'Login is invalid'); + + redirect('users/login'); + } + } + } + + // Log user out + public function logout(){ + // Unset user data + $this->session->unset_userdata('logged_in'); + $this->session->unset_userdata('user_id'); + $this->session->unset_userdata('username'); + + // Set message + $this->session->set_flashdata('user_loggedout', 'You are now logged out'); + + redirect('users/login'); + } + + // Check if username exists + public function check_username_exists($username){ + $this->form_validation->set_message('check_username_exists', 'That username is taken. Please choose a different one'); + if($this->user_model->check_username_exists($username)){ + return true; + } else { + return false; + } + } + + // Check if email exists + public function check_email_exists($email){ + $this->form_validation->set_message('check_email_exists', 'That email is taken. Please choose a different one'); + if($this->user_model->check_email_exists($email)){ + return true; + } else { + return false; + } + } + } \ No newline at end of file diff --git a/application/controllers/Welcome.php b/application/controllers/Welcome.php new file mode 100755 index 0000000..5f82771 --- /dev/null +++ b/application/controllers/Welcome.php @@ -0,0 +1,25 @@ + + * @see https://codeigniter.com/userguide3/general/urls.html + */ + public function index() + { + $this->load->view('welcome_message'); + } +} diff --git a/application/controllers/index.html b/application/controllers/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/controllers/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/core/index.html b/application/core/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/core/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/helpers/index.html b/application/helpers/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/helpers/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/hooks/index.html b/application/hooks/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/hooks/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/index.html b/application/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/language/english/index.html b/application/language/english/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/language/english/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/language/index.html b/application/language/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/language/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/libraries/index.html b/application/libraries/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/libraries/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/logs/index.html b/application/logs/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/logs/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/models/Category_model.php b/application/models/Category_model.php new file mode 100644 index 0000000..d40e2f1 --- /dev/null +++ b/application/models/Category_model.php @@ -0,0 +1,38 @@ +load->database(); + } + + public function get_categories(){ + $this->db->order_by('name'); + $query = $this->db->get('categories'); + return $query->result_array(); + } + + public function create_category(){ + $data = array( + 'name' => $this->input->post('name'), + 'user_id' => $this->session->userdata('user_id') + ); + + return $this->db->insert('categories', $data); + } + + public function get_category($category_id){ + $query = $this->db->get_where('categories',array('id' => $category_id)); + return $query->row(); + } + + public function delete_category($id){ + $this->db->where('id', $id); + $this->db->delete('categories'); + return true; + } + +} + + + diff --git a/application/models/Comment_model.php b/application/models/Comment_model.php new file mode 100644 index 0000000..0e94ffd --- /dev/null +++ b/application/models/Comment_model.php @@ -0,0 +1,24 @@ +load->database(); + } + + public function create_comment($post_id) + { + $data = array( + 'post_id' => $post_id, + 'name' => $this->input->post('name'), + 'email' => $this->input->post('email'), + 'body' => $this->input->post('body'), + ); + + return $this->db->insert('comments', $data); + } + + public function get_comments($post_id) + { + $query = $this->db->get_where('comments', array('post_id' => $post_id)); + return $query->result_array(); + } + } \ No newline at end of file diff --git a/application/models/Post_model.php b/application/models/Post_model.php new file mode 100644 index 0000000..ac2b04c --- /dev/null +++ b/application/models/Post_model.php @@ -0,0 +1,71 @@ +load->database(); + } + + //this is the function to get the posts + public function get_posts($slug = FALSE, $limit = FALSE, $offset = FALSE){ + if($limit){ + $this->db->limit($limit,$offset); + } + if($slug === FALSE){ + $this->db->order_by('posts.id','DESC'); + $this->db->join('categories','categories.id = posts.category_id'); + $query = $this->db->get('posts'); + return $query->result_array(); + } + $query = $this->db->get_where('posts', array('slug' => $slug)); + return $query->row_array(); + } + + //function to create posts + public function create_post($post_image){ + $slug = url_title($this->input->post('title')); + $data = array( + 'title' => $this->input->post('title'), + 'slug' => $slug, + 'body' => $this->input->post('body'), + 'category_id' => $this->input->post('category_id'), + 'user_id' => $this->session->userdata('user_id'), + 'post_image' => $post_image + ); + return $this->db->insert('posts', $data); + } + + //function to delete posts + public function delete_post($id){ + $this->db->where('id', $id); + $this->db->delete('posts'); + return true; + } + + //function to update posts + public function update_post(){ + //the post method of codeigniter retrieves the data from the POST when we submit the input + $slug = url_title($this->input->post('title')); + $data = array( + 'title' => $this->input->post('title'), + 'slug' => $slug, + 'body' => $this->input->post('body') + ); + $this->db->where('id',$this->input->post('id')); + return $this->db->update('posts', $data); + //update is an inbuilt codeigniter method + } + + public function get_categories(){ + $this->db->order_by('name'); + $query = $this->db->get('categories'); + return $query->result_array(); + } + + public function get_posts_by_category($category_id){ + $this->db->order_by('category_id','DESC'); + $this->db->join('categories','categories.id = posts.category_id'); + $query = $this->db->get_where('posts', array('category_id' => $category_id)); + return $query->result_array(); + } + + } \ No newline at end of file diff --git a/application/models/User_model.php b/application/models/User_model.php new file mode 100644 index 0000000..24aa8a9 --- /dev/null +++ b/application/models/User_model.php @@ -0,0 +1,51 @@ + $this->input->post('name'), + 'email' => $this->input->post('email'), + 'username' => $this->input->post('username'), + 'password' => $enc_password, + 'zipcode' => $this->input->post('zipcode') + ); + + // Insert user + return $this->db->insert('users', $data); + } + + // Log user in + public function login($username, $password){ + // Validate + $this->db->where('username', $username); + $this->db->where('password', $password); + + $result = $this->db->get('users'); + + if($result->num_rows() == 1){ + return $result->row(0)->id; + } else { + return false; + } + } + + // Check username exists + public function check_username_exists($username){ + $query = $this->db->get_where('users', array('username' => $username)); + if(empty($query->row_array())){ + return true; + } else { + return false; + } + } + + // Check email exists + public function check_email_exists($email){ + $query = $this->db->get_where('users', array('email' => $email)); + if(empty($query->row_array())){ + return true; + } else { + return false; + } + } + } \ No newline at end of file diff --git a/application/models/index.html b/application/models/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/models/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/third_party/index.html b/application/third_party/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/third_party/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/views/categories/create.php b/application/views/categories/create.php new file mode 100644 index 0000000..d3ab389 --- /dev/null +++ b/application/views/categories/create.php @@ -0,0 +1,13 @@ +

+ + + + +
+ + +
+ + + + diff --git a/application/views/categories/index.php b/application/views/categories/index.php new file mode 100644 index 0000000..11b833e --- /dev/null +++ b/application/views/categories/index.php @@ -0,0 +1,12 @@ +

+ \ No newline at end of file diff --git a/application/views/errors/cli/error_404.php b/application/views/errors/cli/error_404.php new file mode 100755 index 0000000..6984b61 --- /dev/null +++ b/application/views/errors/cli/error_404.php @@ -0,0 +1,8 @@ + + +An uncaught Exception was encountered + +Type: +Message: +Filename: getFile(), "\n"; ?> +Line Number: getLine(); ?> + + + +Backtrace: +getTrace() as $error): ?> + + File: + Line: + Function: + + + + diff --git a/application/views/errors/cli/error_general.php b/application/views/errors/cli/error_general.php new file mode 100755 index 0000000..6984b61 --- /dev/null +++ b/application/views/errors/cli/error_general.php @@ -0,0 +1,8 @@ + + +A PHP Error was encountered + +Severity: +Message: +Filename: +Line Number: + + + +Backtrace: + + + File: + Line: + Function: + + + + diff --git a/application/views/errors/cli/index.html b/application/views/errors/cli/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/views/errors/cli/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/views/errors/html/error_404.php b/application/views/errors/html/error_404.php new file mode 100755 index 0000000..756ea9d --- /dev/null +++ b/application/views/errors/html/error_404.php @@ -0,0 +1,64 @@ + + + + +404 Page Not Found + + + +
+

+ +
+ + \ No newline at end of file diff --git a/application/views/errors/html/error_db.php b/application/views/errors/html/error_db.php new file mode 100755 index 0000000..f5a43f6 --- /dev/null +++ b/application/views/errors/html/error_db.php @@ -0,0 +1,64 @@ + + + + +Database Error + + + +
+

+ +
+ + \ No newline at end of file diff --git a/application/views/errors/html/error_exception.php b/application/views/errors/html/error_exception.php new file mode 100755 index 0000000..8784886 --- /dev/null +++ b/application/views/errors/html/error_exception.php @@ -0,0 +1,32 @@ + + +
+ +

An uncaught Exception was encountered

+ +

Type:

+

Message:

+

Filename: getFile(); ?>

+

Line Number: getLine(); ?>

+ + + +

Backtrace:

+ getTrace() as $error): ?> + + + +

+ File:
+ Line:
+ Function: +

+ + + + + + +
\ No newline at end of file diff --git a/application/views/errors/html/error_general.php b/application/views/errors/html/error_general.php new file mode 100755 index 0000000..fc3b2eb --- /dev/null +++ b/application/views/errors/html/error_general.php @@ -0,0 +1,64 @@ + + + + +Error + + + +
+

+ +
+ + \ No newline at end of file diff --git a/application/views/errors/html/error_php.php b/application/views/errors/html/error_php.php new file mode 100755 index 0000000..b146f9c --- /dev/null +++ b/application/views/errors/html/error_php.php @@ -0,0 +1,33 @@ + + +
+ +

A PHP Error was encountered

+ +

Severity:

+

Message:

+

Filename:

+

Line Number:

+ + + +

Backtrace:

+ + + + +

+ File:
+ Line:
+ Function: +

+ + + + + + + +
\ No newline at end of file diff --git a/application/views/errors/html/index.html b/application/views/errors/html/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/views/errors/html/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/views/errors/index.html b/application/views/errors/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/views/errors/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/views/index.html b/application/views/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/application/views/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/views/pages/about.php b/application/views/pages/about.php new file mode 100644 index 0000000..9cd1787 --- /dev/null +++ b/application/views/pages/about.php @@ -0,0 +1,2 @@ +

+

This is blog version 1.0

\ No newline at end of file diff --git a/application/views/pages/home.php b/application/views/pages/home.php new file mode 100644 index 0000000..8b157fd --- /dev/null +++ b/application/views/pages/home.php @@ -0,0 +1,2 @@ +

+

Welcome to the Blog Application

\ No newline at end of file diff --git a/application/views/posts/create.php b/application/views/posts/create.php new file mode 100644 index 0000000..97cce69 --- /dev/null +++ b/application/views/posts/create.php @@ -0,0 +1,29 @@ +

+ + + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + \ No newline at end of file diff --git a/application/views/posts/edit.php b/application/views/posts/edit.php new file mode 100644 index 0000000..9cbe4fe --- /dev/null +++ b/application/views/posts/edit.php @@ -0,0 +1,26 @@ +

+ + + + + + +
+ + +
+
+ + +
+
+ + +
+ + \ No newline at end of file diff --git a/application/views/posts/index.php b/application/views/posts/index.php new file mode 100644 index 0000000..b15ca28 --- /dev/null +++ b/application/views/posts/index.php @@ -0,0 +1,22 @@ + +

+ + +

+
+
+ +
+
+
+ +

+

+ Read More +

+
+
+ + diff --git a/application/views/posts/view.php b/application/views/posts/view.php new file mode 100644 index 0000000..44c81a0 --- /dev/null +++ b/application/views/posts/view.php @@ -0,0 +1,48 @@ + +

+
+ +
+ +
+ +
+session->userdata('user_id') == $post['user_id']): ?> +
+ Edit + + + + +
+

Comments

+ + +
+
[by ]
+
+ + +

No Comments to display

+ +
+

Add Comments

+ + +
+ + +
+
+ + +
+
+ + +
+ + + + + diff --git a/application/views/templates/footer.php b/application/views/templates/footer.php new file mode 100644 index 0000000..bdec4ca --- /dev/null +++ b/application/views/templates/footer.php @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/application/views/templates/header.php b/application/views/templates/header.php new file mode 100644 index 0000000..c3642f3 --- /dev/null +++ b/application/views/templates/header.php @@ -0,0 +1,76 @@ + + + + + + Blog App + + + + + + + +
+ +session->flashdata('user_registered')): ?> + '.$this->session->flashdata('user_registered').'

'; ?> + + +session->flashdata('post_created')): ?> + '.$this->session->flashdata('post_created').'

'; ?> + + +session->flashdata('post_updated')): ?> + '.$this->session->flashdata('post_updated').'

'; ?> + + +session->flashdata('category_registered')): ?> + '.$this->session->flashdata('category_created').'

'; ?> + + +session->flashdata('post_deleted')): ?> + '.$this->session->flashdata('post_deleted').'

'; ?> + + +session->flashdata('login_failed')): ?> + '.$this->session->flashdata('login_failed').'

'; ?> + + +session->flashdata('user_loggedin')): ?> + '.$this->session->flashdata('user_loggedin').'

'; ?> + + +session->flashdata('user_loggedout')): ?> + '.$this->session->flashdata('user_loggedout').'

'; ?> + + +session->flashdata('category_deleted')): ?> + '.$this->session->flashdata('category_deleted').'

'; ?> + + diff --git a/application/views/users/login.php b/application/views/users/login.php new file mode 100644 index 0000000..12ff884 --- /dev/null +++ b/application/views/users/login.php @@ -0,0 +1,14 @@ + +
+
+

+
+ +
+
+ +
+ +
+
+ \ No newline at end of file diff --git a/application/views/users/register.php b/application/views/users/register.php new file mode 100644 index 0000000..ddb184b --- /dev/null +++ b/application/views/users/register.php @@ -0,0 +1,34 @@ + + + +
+
+

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ \ No newline at end of file diff --git a/application/views/welcome_message.php b/application/views/welcome_message.php new file mode 100755 index 0000000..9db22bc --- /dev/null +++ b/application/views/welcome_message.php @@ -0,0 +1,100 @@ + + + + + Welcome to CodeIgniter + + + + + +
+

Welcome to CodeIgniter!

+ +
+

The page you are looking at is being generated dynamically by CodeIgniter.

+ +

If you would like to edit this page you'll find it located at:

+ application/views/welcome_message.php + +

The corresponding controller for this page is found at:

+ application/controllers/Welcome.php + +

If you are exploring CodeIgniter for the very first time, you should start by reading the User Guide.

+
+ + +
+ + + diff --git a/assets/css/bootstrap.min.css b/assets/css/bootstrap.min.css new file mode 100644 index 0000000..c11e949 --- /dev/null +++ b/assets/css/bootstrap.min.css @@ -0,0 +1,11 @@ +@import url("https://fonts.googleapis.com/css?family=Lato:400,700,400italic&display=swap");/*! + * bootswatch v3.4.1 + * Homepage: https://bootswatch.com + * Copyright 2012-2019 Thomas Park + * Licensed under MIT + * Based on Bootstrap +*//*! + * Bootstrap v3.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,*:before,*:after{color:#000 !important;text-shadow:none !important;background:transparent !important;box-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}@font-face{font-family:"Glyphicons Halflings";src:url("../fonts/glyphicons-halflings-regular.eot");src:url("../fonts/glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2"),url("../fonts/glyphicons-halflings-regular.woff") format("woff"),url("../fonts/glyphicons-halflings-regular.ttf") format("truetype"),url("../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular") format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{box-sizing:border-box}*:before,*:after{box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Lato","Helvetica Neue",Helvetica,Arial,sans-serif;font-size:15px;line-height:1.42857143;color:#2c3e50;background-color:#ffffff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#18bc9c;text-decoration:none}a:hover,a:focus{color:#18bc9c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#ffffff;border:1px solid #ecf0f1;border-radius:4px;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:21px;margin-bottom:21px;border:0;border-top:1px solid #ecf0f1}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Lato","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:400;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#b4bcc2}h1,.h1,h2,.h2,h3,.h3{margin-top:21px;margin-bottom:10.5px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10.5px;margin-bottom:10.5px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:39px}h2,.h2{font-size:32px}h3,.h3{font-size:26px}h4,.h4{font-size:19px}h5,.h5{font-size:15px}h6,.h6{font-size:13px}p{margin:0 0 10.5px}.lead{margin-bottom:21px;font-size:17px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:22.5px}}small,.small{font-size:86%}mark,.mark{padding:.2em;background-color:#f39c12}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#b4bcc2}.text-primary{color:#2c3e50}a.text-primary:hover,a.text-primary:focus{color:#1a242f}.text-success{color:#ffffff}a.text-success:hover,a.text-success:focus{color:#e6e6e6}.text-info{color:#ffffff}a.text-info:hover,a.text-info:focus{color:#e6e6e6}.text-warning{color:#ffffff}a.text-warning:hover,a.text-warning:focus{color:#e6e6e6}.text-danger{color:#ffffff}a.text-danger:hover,a.text-danger:focus{color:#e6e6e6}.bg-primary{color:#fff;background-color:#2c3e50}a.bg-primary:hover,a.bg-primary:focus{background-color:#1a242f}.bg-success{background-color:#18bc9c}a.bg-success:hover,a.bg-success:focus{background-color:#128f76}.bg-info{background-color:#3498db}a.bg-info:hover,a.bg-info:focus{background-color:#217dbb}.bg-warning{background-color:#f39c12}a.bg-warning:hover,a.bg-warning:focus{background-color:#c87f0a}.bg-danger{background-color:#e74c3c}a.bg-danger:hover,a.bg-danger:focus{background-color:#d62c1a}.page-header{padding-bottom:9.5px;margin:42px 0 21px;border-bottom:1px solid transparent}ul,ol{margin-top:0;margin-bottom:10.5px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:21px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10.5px 21px;margin:0 0 21px;font-size:18.75px;border-left:5px solid #ecf0f1}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#b4bcc2}blockquote footer:before,blockquote small:before,blockquote .small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #ecf0f1;border-left:0}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:""}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:"\00A0 \2014"}address{margin-bottom:21px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#ffffff;background-color:#333333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;box-shadow:none}pre{display:block;padding:10px;margin:0 0 10.5px;font-size:14px;line-height:1.42857143;color:#7b8a8b;word-break:break-all;word-wrap:break-word;background-color:#ecf0f1;border:1px solid #cccccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*="col-"]{padding-right:0;padding-left:0}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0%}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0%}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0%}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0%}}table{background-color:transparent}table col[class*="col-"]{position:static;display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#b4bcc2;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:21px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ecf0f1}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ecf0f1}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ecf0f1}.table .table{background-color:#ffffff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ecf0f1}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ecf0f1}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#ecf0f1}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#ecf0f1}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#dde4e6}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#18bc9c}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#15a589}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#3498db}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#258cd1}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#f39c12}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#e08e0b}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#e74c3c}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#e43725}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15.75px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ecf0f1}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:21px;font-size:22.5px;line-height:inherit;color:#2c3e50;border:0;border-bottom:1px solid transparent}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type="search"]{box-sizing:border-box;-webkit-appearance:none;appearance:none}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:11px;font-size:15px;line-height:1.42857143;color:#2c3e50}.form-control{display:block;width:100%;height:45px;padding:10px 15px;font-size:15px;line-height:1.42857143;color:#2c3e50;background-color:#ffffff;background-image:none;border:1px solid #dce4ec;border-radius:4px;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#2c3e50;outline:0;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(44,62,80,0.6)}.form-control::-moz-placeholder{color:#acb6c0;opacity:1}.form-control:-ms-input-placeholder{color:#acb6c0}.form-control::-webkit-input-placeholder{color:#acb6c0}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#ecf0f1;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:45px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:35px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:66px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.radio label,.checkbox label{min-height:21px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-top:4px \9;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:36px;padding-top:11px;padding-bottom:11px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:35px;padding:6px 9px;font-size:13px;line-height:1.5;border-radius:3px}select.input-sm{height:35px;line-height:35px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:35px;padding:6px 9px;font-size:13px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:35px;line-height:35px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:35px;min-height:34px;padding:7px 9px;font-size:13px;line-height:1.5}.input-lg{height:66px;padding:18px 27px;font-size:19px;line-height:1.3333333;border-radius:6px}select.input-lg{height:66px;line-height:66px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:66px;padding:18px 27px;font-size:19px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:66px;line-height:66px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:66px;min-height:40px;padding:19px 27px;font-size:19px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:56.25px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:45px;height:45px;line-height:45px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:66px;height:66px;line-height:66px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:35px;height:35px;line-height:35px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#ffffff}.has-success .form-control{border-color:#ffffff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#e6e6e6;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-success .input-group-addon{color:#ffffff;background-color:#18bc9c;border-color:#ffffff}.has-success .form-control-feedback{color:#ffffff}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#ffffff}.has-warning .form-control{border-color:#ffffff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#e6e6e6;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-warning .input-group-addon{color:#ffffff;background-color:#f39c12;border-color:#ffffff}.has-warning .form-control-feedback{color:#ffffff}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#ffffff}.has-error .form-control{border-color:#ffffff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#e6e6e6;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-error .input-group-addon{color:#ffffff;background-color:#e74c3c;border-color:#ffffff}.has-error .form-control-feedback{color:#ffffff}.has-feedback label~.form-control-feedback{top:26px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#597ea2}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:11px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:32px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:11px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:19px;font-size:19px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:7px;font-size:13px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:10px 15px;font-size:15px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#ffffff;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);opacity:0.65;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#ffffff;background-color:#95a5a6;border-color:#95a5a6}.btn-default:focus,.btn-default.focus{color:#ffffff;background-color:#798d8f;border-color:#566566}.btn-default:hover{color:#ffffff;background-color:#798d8f;border-color:#74898a}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#ffffff;background-color:#798d8f;background-image:none;border-color:#74898a}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#ffffff;background-color:#687b7c;border-color:#566566}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#95a5a6;border-color:#95a5a6}.btn-default .badge{color:#95a5a6;background-color:#ffffff}.btn-primary{color:#ffffff;background-color:#2c3e50;border-color:#2c3e50}.btn-primary:focus,.btn-primary.focus{color:#ffffff;background-color:#1a242f;border-color:#000000}.btn-primary:hover{color:#ffffff;background-color:#1a242f;border-color:#161f29}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#ffffff;background-color:#1a242f;background-image:none;border-color:#161f29}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#ffffff;background-color:#0d1318;border-color:#000000}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#2c3e50;border-color:#2c3e50}.btn-primary .badge{color:#2c3e50;background-color:#ffffff}.btn-success{color:#ffffff;background-color:#18bc9c;border-color:#18bc9c}.btn-success:focus,.btn-success.focus{color:#ffffff;background-color:#128f76;border-color:#0a4b3e}.btn-success:hover{color:#ffffff;background-color:#128f76;border-color:#11866f}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#ffffff;background-color:#128f76;background-image:none;border-color:#11866f}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#ffffff;background-color:#0e6f5c;border-color:#0a4b3e}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#18bc9c;border-color:#18bc9c}.btn-success .badge{color:#18bc9c;background-color:#ffffff}.btn-info{color:#ffffff;background-color:#3498db;border-color:#3498db}.btn-info:focus,.btn-info.focus{color:#ffffff;background-color:#217dbb;border-color:#16527a}.btn-info:hover{color:#ffffff;background-color:#217dbb;border-color:#2077b2}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#ffffff;background-color:#217dbb;background-image:none;border-color:#2077b2}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#ffffff;background-color:#1c699d;border-color:#16527a}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#3498db;border-color:#3498db}.btn-info .badge{color:#3498db;background-color:#ffffff}.btn-warning{color:#ffffff;background-color:#f39c12;border-color:#f39c12}.btn-warning:focus,.btn-warning.focus{color:#ffffff;background-color:#c87f0a;border-color:#7f5006}.btn-warning:hover{color:#ffffff;background-color:#c87f0a;border-color:#be780a}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#ffffff;background-color:#c87f0a;background-image:none;border-color:#be780a}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#ffffff;background-color:#a66908;border-color:#7f5006}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#f39c12;border-color:#f39c12}.btn-warning .badge{color:#f39c12;background-color:#ffffff}.btn-danger{color:#ffffff;background-color:#e74c3c;border-color:#e74c3c}.btn-danger:focus,.btn-danger.focus{color:#ffffff;background-color:#d62c1a;border-color:#921e12}.btn-danger:hover{color:#ffffff;background-color:#d62c1a;border-color:#cd2a19}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#ffffff;background-color:#d62c1a;background-image:none;border-color:#cd2a19}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#ffffff;background-color:#b62516;border-color:#921e12}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#e74c3c;border-color:#e74c3c}.btn-danger .badge{color:#e74c3c;background-color:#ffffff}.btn-link{font-weight:400;color:#18bc9c;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#18bc9c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#b4bcc2;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:18px 27px;font-size:19px;line-height:1.3333333;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:6px 9px;font-size:13px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:13px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;transition:opacity 0.15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;transition-property:height, visibility;transition-duration:0.35s;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid \9;border-right:4px solid transparent;border-left:4px solid transparent}.dropup,.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:15px;text-align:left;list-style:none;background-color:#ffffff;background-clip:padding-box;border:1px solid #cccccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;box-shadow:0 6px 12px rgba(0,0,0,0.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#7b8a8b;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#ffffff;text-decoration:none;background-color:#2c3e50}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#ffffff;text-decoration:none;background-color:#2c3e50;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#b4bcc2}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:13px;line-height:1.42857143;color:#b4bcc2;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid \9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:66px;padding:18px 27px;font-size:19px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:66px;line-height:66px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:35px;padding:6px 9px;font-size:13px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:35px;line-height:35px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:10px 15px;font-size:15px;font-weight:400;line-height:1;color:#2c3e50;text-align:center;background-color:#ecf0f1;border:1px solid #dce4ec;border-radius:4px}.input-group-addon.input-sm{padding:6px 9px;font-size:13px;border-radius:3px}.input-group-addon.input-lg{padding:18px 27px;font-size:19px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#ecf0f1}.nav>li.disabled>a{color:#b4bcc2}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#b4bcc2;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#ecf0f1;border-color:#18bc9c}.nav .nav-divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ecf0f1}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#ecf0f1 #ecf0f1 #ecf0f1}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#2c3e50;cursor:default;background-color:#ffffff;border:1px solid #ecf0f1;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ecf0f1}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ecf0f1;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#ffffff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#ffffff;background-color:#2c3e50}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ecf0f1}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ecf0f1;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#ffffff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:60px;margin-bottom:21px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:60px;padding:19.5px 15px;font-size:19px;line-height:21px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:13px;margin-bottom:13px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:9.75px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:21px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:21px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:19.5px;padding-bottom:19.5px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:7.5px;margin-bottom:7.5px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:7.5px;margin-bottom:7.5px}.navbar-btn.btn-sm{margin-top:12.5px;margin-bottom:12.5px}.navbar-btn.btn-xs{margin-top:19px;margin-bottom:19px}.navbar-text{margin-top:19.5px;margin-bottom:19.5px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#2c3e50;border-color:transparent}.navbar-default .navbar-brand{color:#ffffff}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#18bc9c;background-color:transparent}.navbar-default .navbar-text{color:#ffffff}.navbar-default .navbar-nav>li>a{color:#ffffff}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#18bc9c;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#ffffff;background-color:#1a242f}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#cccccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#ffffff;background-color:#1a242f}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#ffffff}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#18bc9c;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#ffffff;background-color:#1a242f}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#cccccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#1a242f}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#1a242f}.navbar-default .navbar-toggle .icon-bar{background-color:#ffffff}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:transparent}.navbar-default .navbar-link{color:#ffffff}.navbar-default .navbar-link:hover{color:#18bc9c}.navbar-default .btn-link{color:#ffffff}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#18bc9c}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#cccccc}.navbar-inverse{background-color:#18bc9c;border-color:transparent}.navbar-inverse .navbar-brand{color:#ffffff}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#2c3e50;background-color:transparent}.navbar-inverse .navbar-text{color:#ffffff}.navbar-inverse .navbar-nav>li>a{color:#ffffff}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#2c3e50;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#ffffff;background-color:#15a589}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#cccccc;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#ffffff;background-color:#15a589}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#ffffff}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#2c3e50;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#ffffff;background-color:#15a589}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#cccccc;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#128f76}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#128f76}.navbar-inverse .navbar-toggle .icon-bar{background-color:#ffffff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#149c82}.navbar-inverse .navbar-link{color:#ffffff}.navbar-inverse .navbar-link:hover{color:#2c3e50}.navbar-inverse .btn-link{color:#ffffff}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#2c3e50}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#cccccc}.breadcrumb{padding:8px 15px;margin-bottom:21px;list-style:none;background-color:#ecf0f1;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#cccccc;content:"/\00a0"}.breadcrumb>.active{color:#95a5a6}.pagination{display:inline-block;padding-left:0;margin:21px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:10px 15px;margin-left:-1px;line-height:1.42857143;color:#ffffff;text-decoration:none;background-color:#18bc9c;border:1px solid transparent}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{z-index:2;color:#ffffff;background-color:#0f7864;border-color:transparent}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:3;color:#ffffff;cursor:default;background-color:#0f7864;border-color:transparent}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#ecf0f1;cursor:not-allowed;background-color:#3be6c4;border-color:transparent}.pagination-lg>li>a,.pagination-lg>li>span{padding:18px 27px;font-size:19px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:6px 9px;font-size:13px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:21px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#18bc9c;border:1px solid transparent;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#0f7864}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#ffffff;cursor:not-allowed;background-color:#18bc9c}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#ffffff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#ffffff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#95a5a6}.label-default[href]:hover,.label-default[href]:focus{background-color:#798d8f}.label-primary{background-color:#2c3e50}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#1a242f}.label-success{background-color:#18bc9c}.label-success[href]:hover,.label-success[href]:focus{background-color:#128f76}.label-info{background-color:#3498db}.label-info[href]:hover,.label-info[href]:focus{background-color:#217dbb}.label-warning{background-color:#f39c12}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#c87f0a}.label-danger{background-color:#e74c3c}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#d62c1a}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:13px;font-weight:bold;line-height:1;color:#ffffff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#2c3e50;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#ffffff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#2c3e50;background-color:#ffffff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#ecf0f1}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:23px;font-weight:200}.jumbotron>hr{border-top-color:#cfd9db}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:68px}}.thumbnail{display:block;padding:4px;margin-bottom:21px;line-height:1.42857143;background-color:#ffffff;border:1px solid #ecf0f1;border-radius:4px;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#18bc9c}.thumbnail .caption{padding:9px;color:#2c3e50}.alert{padding:15px;margin-bottom:21px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#ffffff;background-color:#18bc9c;border-color:#18bc9c}.alert-success hr{border-top-color:#15a589}.alert-success .alert-link{color:#e6e6e6}.alert-info{color:#ffffff;background-color:#3498db;border-color:#3498db}.alert-info hr{border-top-color:#258cd1}.alert-info .alert-link{color:#e6e6e6}.alert-warning{color:#ffffff;background-color:#f39c12;border-color:#f39c12}.alert-warning hr{border-top-color:#e08e0b}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{color:#ffffff;background-color:#e74c3c;border-color:#e74c3c}.alert-danger hr{border-top-color:#e43725}.alert-danger .alert-link{color:#e6e6e6}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:21px;margin-bottom:21px;overflow:hidden;background-color:#ecf0f1;border-radius:4px;box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0%;height:100%;font-size:13px;line-height:21px;color:#ffffff;text-align:center;background-color:#2c3e50;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);transition:width 0.6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#18bc9c}.progress-striped .progress-bar-success{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#3498db}.progress-striped .progress-bar-info{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#f39c12}.progress-striped .progress-bar-warning{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#e74c3c}.progress-striped .progress-bar-danger{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#ffffff;border:1px solid #ecf0f1}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{color:#b4bcc2;cursor:not-allowed;background-color:#ecf0f1}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#b4bcc2}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#ffffff;background-color:#2c3e50;border-color:#2c3e50}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#8aa4be}a.list-group-item,button.list-group-item{color:#555555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{color:#555555;text-decoration:none;background-color:#ecf0f1}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#ffffff;background-color:#18bc9c}a.list-group-item-success,button.list-group-item-success{color:#ffffff}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#ffffff;background-color:#15a589}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.list-group-item-info{color:#ffffff;background-color:#3498db}a.list-group-item-info,button.list-group-item-info{color:#ffffff}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#ffffff;background-color:#258cd1}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.list-group-item-warning{color:#ffffff;background-color:#f39c12}a.list-group-item-warning,button.list-group-item-warning{color:#ffffff}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#ffffff;background-color:#e08e0b}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.list-group-item-danger{color:#ffffff;background-color:#e74c3c}a.list-group-item-danger,button.list-group-item-danger{color:#ffffff}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#ffffff;background-color:#e43725}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:21px;background-color:#ffffff;border:1px solid transparent;border-radius:4px;box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:17px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#ecf0f1;border-top:1px solid #ecf0f1;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-right:15px;padding-left:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ecf0f1}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:21px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #ecf0f1}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ecf0f1}.panel-default{border-color:#ecf0f1}.panel-default>.panel-heading{color:#2c3e50;background-color:#ecf0f1;border-color:#ecf0f1}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ecf0f1}.panel-default>.panel-heading .badge{color:#ecf0f1;background-color:#2c3e50}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ecf0f1}.panel-primary{border-color:#2c3e50}.panel-primary>.panel-heading{color:#ffffff;background-color:#2c3e50;border-color:#2c3e50}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#2c3e50}.panel-primary>.panel-heading .badge{color:#2c3e50;background-color:#ffffff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#2c3e50}.panel-success{border-color:#18bc9c}.panel-success>.panel-heading{color:#ffffff;background-color:#18bc9c;border-color:#18bc9c}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#18bc9c}.panel-success>.panel-heading .badge{color:#18bc9c;background-color:#ffffff}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#18bc9c}.panel-info{border-color:#3498db}.panel-info>.panel-heading{color:#ffffff;background-color:#3498db;border-color:#3498db}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#3498db}.panel-info>.panel-heading .badge{color:#3498db;background-color:#ffffff}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#3498db}.panel-warning{border-color:#f39c12}.panel-warning>.panel-heading{color:#ffffff;background-color:#f39c12;border-color:#f39c12}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#f39c12}.panel-warning>.panel-heading .badge{color:#f39c12;background-color:#ffffff}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#f39c12}.panel-danger{border-color:#e74c3c}.panel-danger>.panel-heading{color:#ffffff;background-color:#e74c3c;border-color:#e74c3c}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#e74c3c}.panel-danger>.panel-heading .badge{color:#e74c3c;background-color:#ffffff}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#e74c3c}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#ecf0f1;border:1px solid transparent;border-radius:4px;box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:22.5px;font-weight:bold;line-height:1;color:#000000;text-shadow:none;filter:alpha(opacity=20);opacity:0.2}.close:hover,.close:focus{color:#000000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:0.5}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none;appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);transform:translate(0, -25%);transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);transform:translate(0, 0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#ffffff;background-clip:padding-box;border:1px solid #999999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;box-shadow:0 3px 9px rgba(0,0,0,0.5);outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:0.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:20px}.modal-footer{padding:20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Lato","Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:13px;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:0.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;background-color:#000000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Lato","Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:15px;background-color:#ffffff;background-clip:padding-box;border:1px solid #cccccc;border:1px solid rgba(0,0,0,0.2);border-radius:6px;box-shadow:0 5px 10px rgba(0,0,0,0.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover>.arrow{border-width:11px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#ffffff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#ffffff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999999;border-bottom-color:rgba(0,0,0,0.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#ffffff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999999;border-left-color:rgba(0,0,0,0.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#ffffff}.popover-title{padding:8px 14px;margin:0;font-size:15px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.next,.carousel-inner>.item.active.right{-webkit-transform:translate3d(100%, 0, 0);transform:translate3d(100%, 0, 0);left:0}.carousel-inner>.item.prev,.carousel-inner>.item.active.left{-webkit-transform:translate3d(-100%, 0, 0);transform:translate3d(-100%, 0, 0);left:0}.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right,.carousel-inner>.item.active{-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:0.5}.carousel-control.left{background-image:linear-gradient(to right, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:linear-gradient(to right, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:hover,.carousel-control:focus{color:#ffffff;text-decoration:none;outline:0;filter:alpha(opacity=90);opacity:0.9}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #ffffff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#ffffff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-header:before,.modal-header:after,.modal-footer:before,.modal-footer:after{display:table;content:" "}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-header:after,.modal-footer:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (max-width:767px){.visible-xs-block{display:block !important}}@media (max-width:767px){.visible-xs-inline{display:inline !important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block !important}table.visible-md{display:table !important}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block !important}}@media (min-width:1200px){.visible-lg{display:block !important}table.visible-lg{display:table !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (min-width:1200px){.visible-lg-block{display:block !important}}@media (min-width:1200px){.visible-lg-inline{display:inline !important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none !important}}@media (min-width:1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}}.navbar{border-width:0}.navbar-default .badge{background-color:#fff;color:#2c3e50}.navbar-inverse .badge{background-color:#fff;color:#18bc9c}.navbar-brand{line-height:1}.btn{border-width:2px}.btn:active{box-shadow:none}.btn-group.open .dropdown-toggle{box-shadow:none}.text-primary,.text-primary:hover{color:#2c3e50}.text-success,.text-success:hover{color:#18bc9c}.text-danger,.text-danger:hover{color:#e74c3c}.text-warning,.text-warning:hover{color:#f39c12}.text-info,.text-info:hover{color:#3498db}table a:not(.btn),.table a:not(.btn){text-decoration:underline}table .dropdown-menu a,.table .dropdown-menu a{text-decoration:none}table .success,.table .success,table .warning,.table .warning,table .danger,.table .danger,table .info,.table .info{color:#fff}table .success>th>a,.table .success>th>a,table .warning>th>a,.table .warning>th>a,table .danger>th>a,.table .danger>th>a,table .info>th>a,.table .info>th>a,table .success>td>a,.table .success>td>a,table .warning>td>a,.table .warning>td>a,table .danger>td>a,.table .danger>td>a,table .info>td>a,.table .info>td>a,table .success>a,.table .success>a,table .warning>a,.table .warning>a,table .danger>a,.table .danger>a,table .info>a,.table .info>a{color:#fff}table>thead>tr>th,.table>thead>tr>th,table>tbody>tr>th,.table>tbody>tr>th,table>tfoot>tr>th,.table>tfoot>tr>th,table>thead>tr>td,.table>thead>tr>td,table>tbody>tr>td,.table>tbody>tr>td,table>tfoot>tr>td,.table>tfoot>tr>td{border:none}table-bordered>thead>tr>th,.table-bordered>thead>tr>th,table-bordered>tbody>tr>th,.table-bordered>tbody>tr>th,table-bordered>tfoot>tr>th,.table-bordered>tfoot>tr>th,table-bordered>thead>tr>td,.table-bordered>thead>tr>td,table-bordered>tbody>tr>td,.table-bordered>tbody>tr>td,table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ecf0f1}.form-control,input{border-width:2px;box-shadow:none}.form-control:focus,input:focus{box-shadow:none}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label,.has-warning .form-control-feedback{color:#f39c12}.has-warning .form-control,.has-warning .form-control:focus{border:2px solid #f39c12}.has-warning .input-group-addon{border-color:#f39c12}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label,.has-error .form-control-feedback{color:#e74c3c}.has-error .form-control,.has-error .form-control:focus{border:2px solid #e74c3c}.has-error .input-group-addon{border-color:#e74c3c}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label,.has-success .form-control-feedback{color:#18bc9c}.has-success .form-control,.has-success .form-control:focus{border:2px solid #18bc9c}.has-success .input-group-addon{border-color:#18bc9c}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{border-color:transparent}.pager a,.pager a:hover{color:#fff}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{background-color:#3be6c4}.close{color:#fff;text-decoration:none;opacity:0.4}.close:hover,.close:focus{color:#fff;opacity:1}.alert .alert-link{color:#fff;text-decoration:underline}.progress{height:10px;box-shadow:none}.progress .progress-bar{font-size:10px;line-height:10px}.well{box-shadow:none}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{border-color:#ecf0f1}a.list-group-item-success.active{background-color:#18bc9c}a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{background-color:#15a589}a.list-group-item-warning.active{background-color:#f39c12}a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{background-color:#e08e0b}a.list-group-item-danger.active{background-color:#e74c3c}a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{background-color:#e43725}.panel-default .close{color:#2c3e50}.modal .close{color:#2c3e50}.popover{color:#2c3e50} \ No newline at end of file diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100755 index 0000000..3085110 --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,30 @@ +.post-date{ + background:#f4f4f4; + padding:4px; + margin:3px 0; + display:block; +} + +.pagination-links{ + margin:30px 0; +} + +.pagination-links strong{ + padding: 8px 13px; + margin:5px; + background: #f4f4f4; + border: 1px #ccc solid; +} + +a.pagination-links{ + padding: 8px 13px; + margin:5px; + background: #f4f4f4; + border: 1px #ccc solid; +} + +.cat-delete{ + display:inline; +} + + diff --git a/assets/images/posts/OS_Ubuntu.png b/assets/images/posts/OS_Ubuntu.png new file mode 100644 index 0000000000000000000000000000000000000000..345e44c3ff14692c27826ee562c84a1d1884fe4f GIT binary patch literal 15196 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 literal 0 HcmV?d00001 diff --git a/assets/images/posts/angular.png b/assets/images/posts/angular.png new file mode 100644 index 0000000000000000000000000000000000000000..a1d9790bc342c674c37a5f4183b5700c22568488 GIT binary patch literal 4509 zcma)Ac{r5a`P>Bb&89Hg@wn&7-a=K zcMd-s$ANFm{7oI;ArfMEBgER@EhOAI$dv`*;(yx}X5#DY?rP=g>=F^!>8j1b!mnq7 zLfG6LozK1OV?QX8-1q5$+vRc z%=$Qp9(O_c=qSfq-vEKCa;3s^8D3@J5>c*yr5tZuMs*MKp6&m%xc^+lU7IjR&Dth4 z&CNxek=r}-WQ7s2m%B?PZ}JMWaO54~J;H)J`kw=)$?~7$|KZL;Bk67Esqc^z^n_=O zKN(_?AYJaGFT^*pXwxpMR~$7fr4H);z4zQy*RB29?5op~D0aOW1+`CttwgisWcnKn z{epP3cvRYp6#68Af|L(>xU{hw25S1}(*5<&^s~4Gt@wPOk?fMk;%K?vH~SjtN;qU- zP`*cp&bQwD1%q`dM)LX+TnCpBRYy$pYHBv;!x616B;LjF{tU_#y=}peAmZo?Us~Ka zRzMK+#!>ln3G)C`_0H%g>DuRh|LjuWframt%N~TP%B>2Xa7DaP+sJ zSBYLw3^iKU0+T{-?RHS@D$I7r5lw$jJA74a)^4m{17eSR*S$&cio;O5+PAg8NYr=7 z&q2%CWYAy(ZB+QBMYmGb@AZ^CELCB@8vY5n{``*y-MZ1dfbmiR5j9xT(SMNW5u6f<< z^~CgxMt(P0*180qL3H8X2)mp7c6p5QD#x4;f70EeST_GIomO;n?_}2WY@S|Y?Mh$1 znXY$5ZthG01wooi;90p8*QqlaMAi8;I^Nasf$iO9-1oE?rdMF+-U9v3>8A_d>_~I3 zFpEPG;UR@LiT9?Z`LdSwXEyDaWB=S~QSKHa1qOtbxUq7sT!MB(LQ99_KVoU&l5dIP zkMGZCaN?1TkBx%B<%|yMYNg2b`iJxbe}$3em@J>^Lb*n6U=fFT>5%m1-iqOct^3nj z%$8s7?!9`_`7)%Om!$@ClJ{w+^O18f{ZWc~KwL$j5Na+tM$njhGK_LW`$A_NcSzdOcW)5X zD#O#i)fFMN;~??z^m~>-#JcG{k<|QIGe#v@9jsHmL_sDr5GLOSER{`*%4(k<&@c*r zhP23C+*L`AVFdG@nWO*AsDG6jT^=(aZ4nGRYBz1R9@}vo0E_7xRg9Q zP^J` z&zD#P6zDjO+1%2(Sdrx|;PNGdm8a%LA32rVe`@)SSCXRP=4thn(n|;_#yafLp4MmR zOzdW?oLZ_2F7iQ|saHd5k*bvD+z+qK`%Xb@!9sbItHF2-1t4)Mekcw3O z$;#kyq|ZqgtCl*;0%x3DWlw#F4CPzd2c{pFa&(1fCJJAr@TJP(P>HmB?^6RhFN{0Y zTuby5=u!H@nHz0gWw0m5*-jk{RbRk~HD8r{U<${_oBjN8lr7t=b~V`dI9Bph>8Hn)e<`uY_VX{AqjBC%S=|OEe zXUhq?T${*KA@`BWo1ko`09JG^4q}9W)0FQ0yHCHJXgiv}78o`#Sr$=3(;;(p-Z?7eMAN{^F0pU2X4@`Ns!6 zqi1Wq5XGTmes^{i*iJ!vF!b^F8&P-0nASDqoz*YT>ul{tnG-^ps_`1~95WP$j|!>K zeQP^f95$ORE4R2Vt?c(Q`s^t;oK^>sM8CY2b3K!g7_eY0BmK~LXiB$O6Raa~#p(7} zg^t&x=N9`qWC5fGm$F}l^V!Pu!u^kO-T0QAok(Tk606?)6qJO8t^x70>wYTxH@qbL z+Hl;q$}wX+<5sTF&P~ED%4D;Ne7~6NyF0t#P<3D)my9Jvpi6ukZX=3Ey!#s4t}nJ2 z*%y?Fqr-5f)HRt7T0{jjFs?YAK2p|3^d&dE9HM&nrMewNP21~`Onp^QeWbx>*OJ2! zv-d~tXBpInp;iy6X{_H zH=<+YtR8+m<{=(9QVyWY!X*wFwo$wHCh&m{D^@um+qm}58oMBdhx~~L6)DZ)wCwGU z5}|Juxu*E2mF22$6U2~P@{CRGdo%lKH2aw-$vI}Qk(f2lj=igoE7hb#rbhq+qzRpE z9QF3xn2_y5L|Tdio4R~gd`Ze`x;M$DLc?E}Ty zuh4%iy6jCVeH=`y36bsSkjSW^*O2p0E-)Lky)-6SFGB_zN(84a)$Z0w+c@FLfDB(* zXP6D=S9@Gaaas*-rV3&;{g*C`vSp>cVQfAO%_X?Y_Ed|5w+iK!UPZ5QROwH9*|syV zP>PM-`Zv$3ox2M)?##%iyGvFDoXhs(KWb9x8cL;m)?TNYHeP!Od9XGDdl+TOXB0Gk z*x~JRvw9cyO(n0bS=q(1WozO#&Cgr-UOAAh$y%$6;RPgsS1#?U;{T#;g*e zR5$yOk-O28bbc;?FG4ag;oh?BzL;aDM54D*!vS>I1`k7j0xC06l+9;Mjkwfht+UL^ zgL8)`mJk554`cKlZahZFK&HQTD@0HV?Z_VP(SKUKLaWzzE{q7$v5Pwit8 zfGbjrMYISbI9snwcL3?SGDG~<5=S#u$)fAI;5Ji;7&x=FKVyG;F>h3}*I2ebQ=w~Yvkc>*2^a5M_^;Rkrxw~#+d=()K{5ezwg zRjjNHZ3$FYJ=EN(#3kEEXYd!M!l-v7xO27g=TWe;oYDY9vHu&$0Sh!H64b0Xf?^%z z;|d)>9*|5BQevkS`oG=NNRHG2H_d+|d@(>=^gGEYYK#1^@84H%OLgCT6qeEsRh4Lo zuwtkb=}%8WJR13%Xx*X?!ovH(tKBAWM?UZgDT{wrJffSeZ335O70(y2wEU zI_jkeJulmUN_}fYNO45w*v48bM0&x!=I_DC z657`s0Fi^@j*c4je@&l#Sf|Wc2`GztE6mX?{j=*-8{=2g|JfdJ!Qt1VFbdv`=TtK8qXsyu&cH z1Ck;CB+I;W*xEPBMwr|_I!yK1Kd$C%fJ%1l8`gHf-QYIB5yEF$Xb-%5o$^RiyC~ug zWbh(lc7u=$@*CSYgnZ-hSZWVpsPS*&PrxapVXm2TLLML;qt}UWV6PGbAjLbjTA$l* zA7|_{dmoO@fk>^DkZ&vye96CYkvVo2RC$lPmcY%dB2e zKEkG0rZ#jM+!R};`*d&h7i~IX2o4?>SCkUNOMPujr?+uA`E}^FW}`w3iw%RsbfjH(`B_=uG1(>UL2^ix+me zcWo0sbRO7C>a$6F@($;}1_xY!4a3n~RCZrX^VO}l@Ji&mmRXK|yeewKb}JAZcq@he zoy`|RwcV0A+JG_4?1Z@VC(}m~AD*qeZ@E61po;2Kh`dml!{xArK?$`wUEn_kcsDJ( zg74lE!qUG=;R<*LB88%>1DU$%bjK^6&r`13wqdAg%i=rvX8Lgs1-Ncnv^L=kWPp*B z?_stDM};qH+e>XJzY-Xzap4oMLjO%YAAuiSV6QX|m~)7+ziuC%@+{`8I#;+fQpZ zwhtJF1XyeZe*Iq6FM`59;}Da3K%acI40cO-Uy(ppK>|o7-z+OW`}e&c`+!VPwj#KTUbmC%~2Hwx8nZ?l$XZ= literal 0 HcmV?d00001 diff --git a/assets/images/posts/bibimbap.png b/assets/images/posts/bibimbap.png new file mode 100644 index 0000000000000000000000000000000000000000..48e33b1cc2111ebfdb629c771bdae65e48a08aa2 GIT binary patch literal 53549 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 jx7iUG9yY`Kbdp^8ypSG4q^&=L8T&={AtJvw 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 O0000e}GXQ`@{oe=ZP(F$S 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 literal 0 HcmV?d00001 diff --git a/assets/images/posts/bitcoin1.png b/assets/images/posts/bitcoin1.png new file mode 100644 index 0000000000000000000000000000000000000000..9638d6054e01d1eb3c03c35d0993a720379e4a1f GIT binary patch literal 28640 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 literal 0 HcmV?d00001 diff --git a/assets/images/posts/ci.png b/assets/images/posts/ci.png new file mode 100644 index 0000000000000000000000000000000000000000..af8bd23ed5c8ec49f13a42eab67deed7e02417f6 GIT binary patch literal 2339 zcmV+;3EcLHP)@hMlB#s&-Y>w8a*>%b$7EsQ+6?U7P+AQV!@ZL2V(1 zt6xFNfo{a?H;^)->%9C8q{`8C9RSv_v z%SZ?3jD)|;^x!fjU8mtUftVoO#Yp!}q>}sq>BjMkKsmYs>Dc(*mI}u}!ckv~v#E)1 zk-n8Yd~DnR>F7)3djQg@_Z6VjxCi=@pFHycNGIQjd;!7_Kt}Y1aFkB~>E;(=L`ENR zzLIwLXbMP&9}f`+FbU*TAI4!lnFP{7@>w7&Hm8B~56kma?I?ggoN`yX3P?B0PXaxL zx~uU|x@q+o>aGSqxSQNLpic&Jrq2O=5Q)V~GEA-BU&M>GV#8jH+)CIadzbWorbE!x zjOgb6u#LBG0cF!cRqd#Z7^r$92X80S#H~4FVQEQiYm&`UARkU7K4GRk{tu2kwde91 zOv`0JES}~!&u*hFy0E}4;pLWMk+$fj*#MICZc$;M_?G4E@Rg^IvtXYE(@r9d=G|X0 z02M~YYmMQ{dUIU3jNf3pngyq>+~4@JwC(uh3x*V1s?y>O6nt#~A6Dr6cr9J+zrcQR zuk#WuqUmsVPJGGp9WBi1UjS0jL5Js9(>#k(gMk-BXRk;V;Ij7BE39~-+Ez_TH-3~* z_lmEc>zSh}`(*-BH(NX z%dM4Ty-R%7p3bU_Q%q4?Ftc08-NyHNMQXVi%x941f{po5S|nt437wf1j1|v~5fj_(+&DB(}1b$z5q@N`>cKkZXoI_Y2C4R zSN|!zyq}yEQf2vRl9OVUkKO~Pvq3X8%4JCWXlc!elVCc|PiBQ2MI!W}@apFq_G`hr9V-8J(R(ib?0%9ru;Uj6Tn9H^o>w zc!!qKwwZ5`-(jip<`vb@cDJJQ+W=7y-tJN&F2J#MV$ zZoSb)?NXrgspw_xXFFKq3}qC{kw$69Q77ENvTHPWr&Boc^Ru;gucIPwoY!8t_q>2g zexfXS;}G?S5l->6U&RXJ{W<+q!I->o1{Pi>aWq8FRN z7fG=A9Lu81fHdpetz1lrQ-H=V0|~01L@6fAfULnw#%$U20JR=QH>r))HXL6QV_CFh z&A`A#2TU6zJ4T1aM~4|SrpiFINnvG@cTOO}8Z2OumSSvx!+YmMY{yNVzSx-#9TUXw zA9VN?FzHL(;+(>Gaa;Mu(vr8&7tpQRYyewC<-P$G}J?2*@ zn_u}M+CTeeA22IT6?xlK)|e2ZnF22;JK8tS0DRyMFZ#Nj$} z&{X}KfN?ftkzLo$e=4Ndlya))D*3iSyjn44l~a{vNycK-Ds024l|j3XB?eIb1kN}g zclcan*i}+R&FNKX((Tehrd_rzqeCL+#!54m0mu!qY-?$j%UnzI_R&sUv|QhJHMW90 zW3I^YZr()IpkBm(n`5bY=zKQf_J4$15Ty{wq~5`{ZILVt-3S2$VK1Q~B8Q#pv~6h- z)qQC99i$wHs_af^oG_QtGJST2} zMOA&^yxQ&zdD6*DF8+oTcBvPccC-tTLz}!ZVW{)8J451OSB|7=%kh*0Q-Fn>m&ID6UagFb0bpxI%EFv4kVBdK*qH zW@$N;2OF#aJE7LiL7DZWya2I&_>C}&_&!8PRtt%EEaM=wTx(AXSu%5xjqG6*xkTFY zH5-tG2X+Jv9E#<;j%Mww*Qx3ATUj|ct9V!k`C>7D@X=-}G6ZEW_E^ro0} z@5H|A)#NB%NaTDy%EhP~Q(_Dp67Rsae9GYa3$|8YkeHBcL&@k;uXwi9t9nDi8DJatPgtUP? zJ$sLsjTWD}M}sy!kVuZVu8LxvNG8`qA0`7e(-@T{gBYePo;$L<3`-J zA+&RC*;5kwe=XJ)i1qh@7~p}+W@11ZcUV`sR%_2a?dQ}Sej8RwANrJUq?8rJ6&on# za{ne`7hCk)5@f(Zaik+CrdUEAH4ZmZEu~JpuOJo!3=l7xHA0=ne8h7mXaU-`KI~Fi zKn_2(Jjl}l;jCP;6UU7ai+Yc*n*yO+et(u|t2F_T#{(Bi0sx8R!&~tsjE4qajsXO< zxL&mwqqX0{JTGg`+dy#HS;-(J4+pH)knyYo0Fu3>b*6Z05y)sqeuDHW3@-(+8-|yV z?uDUtr0(hCaJ|B|?d&=}^fWA+{Lyw?MvfJhv(dKkaGK+X;jpxp7Y+y+vELL>JvmXS zo;e`-jFkjtLAZ&`_NzaT7UtSWgkcI_5bS?#L9*NZw^_RQ{{yvtfP;&xw5&5xN` zQ}pRm{p_dq-d$CvOPiO)mo)&Aw78Tw00a~OFaiDnUcLiF0FdC|;1J-D5D<_skigFy zXh=xtH*m19Z(w2Jkl=m=Bsc^_Bt!%_R8%xHR8%}%TwFZDKf|jM5GW`pcsO`0BqS^x zbYyg#KZXChk(XWoGBn5~$R!vEG5{1A1PmGEr5}I`00Dr4fq(%13J{Q>VBk=%LL49t zl>J=@1`YuV0{OB8fCB?2L;^zs06?w;|4RISIrib~((~UAPR|Z&o#n&--#ZLE%7l26o<-c->>+HnLbU}Tc zSUZpG|BV6XeqDKyj^!|ge`76wZ07fG9AV>KadT~eO+uvl%*MYlK>4Fn@Z^X97?Z^V zqt*YW!eK6}Be?itQV^iknT%_a@h@7S@TCrJJ>4&M0U$Q_Hacym|I`E0C*2Q8kvz!< z0I(dSo6m0ilL8>EKN2$DeGAMEd^N*5Z%BoIa*$K!e=?tB0ez6RR8CvS`6q*vwX)av zwctPmn$lN2YyQCicX^|r(F*mNRLduU(5C$#46vAjt3!H$tk>!R>3v~+Z)5Nej%8%) z!lbp~pde5JpLu3=Z(|SGEa&jPn|}KW{Vljxu34SWr@SH%$-9o8Hll#2iKaJkKE`TM zRhdkGV*&u6qzs4Rs%roMTjCjh?gjJr1TyCdLCr)O@noa?wv%gdUV;XggL2#}tf zwEl201pwHkQS;%{v|x4v;4cPd-|C#oOoHbiq$4dh)w7;_{JSOEW}FNcb0AW4ylt08 z(=~C>{;L*3$%WZ?-FdhGUHCIV!PBBY@&O%%d)QvMY@Ss3hPhsE`%C=Kny1$3rJmN_ z4J<+q;<_gU1$V#xnYBXT(+e0f7ANDUOMXRvI{~X!Ti`3eIfw1)4epPW<3sx!{$#gt!tAD%F95_Wng*BB)Hct@UBqf$ z?#Q+jGLP!%o!9KQyqC5AOU=(hqpr{*K;NoSTO%s@1VQmFfX6Fx{@^KYwdWrfRTLD z6V;M^KVZUSuUps5w|^Bj1R<(tKEXt>ATa-Hg%LVVA@%MKwX-q1O1c3cJ9h#iy2ZeZ zdSm(+`8f;gPd~v%&-trRI@SOH?WzsUqpdqNfq6fu3Y*8EjIV+Kkm{`VF4X;IqYMC` z)zV12fw}w7lmx~JR|=}tegM>KrzckJtp+E`Si`3x0?${wUfJkvRX{3{m4(NO#IzrP zAhh}-;rimQFps`TCU%(hW##3wXjya;1tBK>)H3prcUnsix1dZ*C!u%z{k6Wl6UaJ*ctHqO-7BK z-YAXJjVQKw*YaKEZ+sBV($XDGU+LFeYwJR-UlIIGcs|Bhz82Aq5&2EMQWoj&2_z?1 z3a7q<&0P8D^;o~w31ASO@7TUwl?ES((2J|T(EODLze#{+w5X>gUHBeOqu3Jb$DCg< zB)``-SGF=Pd>o@ucD~Jrm6gBc5s>kk!2;v=^g1`YTE5(tE%Con!M-!wdKc1Mt25rY zzh-_y^b7ps1O?|b#p)A*(Q)!_;Z8Gk?-yRK_!U@VZ4V+lv(S@{4gTQ%{*A=mK!f(# zmb2EJPT>_&slVF0d*JYDaeadGe){rXYyc<&u6Lc#YkdZQ<7+Q*$NS9tOtkzr>;J^{ zMuY`UEB3>9z{&?5j26;#{Y;Hxvs1CRDwr%csZwRba$4!x^~CfZ6cs0Y=4YOV4CYJSnjz>L9HsDE=GsU62Re5l3;PGtUUKeK9Qu67w5XYxA(z4qA%#YP7EVk;3r@{WtA=6@y z&h}pYT(HRO=RCFVBYPE3_pIn64jvzxNjax3YL zvU}fIeT~pcn6F1O)Yv#iBxYhlG!#J3-$Y%|n&Cpg!>Xz^7_;^~s9F0?$_|~v8T&l3pvS-vg-`2EZ zc(D9=*F>qFlgA{oQ|sw}x=AU`eTPBzknPvU#;*X`=~=BZje+a2{3!KC?|Rj}MtfpC z#@%Yc@n-nJx#D|DP{JU>BSL2>=2P0)Phg$$#&M zgMom8LjaJFQBaAP1Qj6B&{^5oiAhPAS^VsQ-EwFk3j_l61rYMdh0@3EE}GJ~iyVix{Ba>WeS(jBsm@dTy7h7#46 z{&oi;neg z&uT)cxdh!{G_E^3Vx-)HHv0*NBF1Ykx=m5jHXU8DO&1BtqnCWb{pQ+li+{A$3{jkB zq^vy0o{)(vR`X+jPN+_l^LO-XjqY~;aLui?%}ubYKn4|0% zejp~!bqkw>>y+p4@#8LW2pfGuGQmxU2Ra77M)oT1;=<=IrQMtC6IKyD4nc+}u+N=! zR?jx@1+mQCfW=_FS{R;yRymQaH0 z;ZDo);3no!9bK!Ln#|77S#72X*?B>|RS{>tu_Ibw8 z4p|B#n7)y$sL|R<_$f8BR(!`F`aWY^q}kt{VuqhPg5@t$4>Q>YDO7`2;JI(Ysv~W? zenawEMAX9(*E)e73o}nI0o_g{YUx{oB^B!z@wik)2GOVnZV@ELc&n#@V@R=1;<$ChR-^wq+Im>K2K2vdGc zjW2{{D)qb4uJ+eHl@y0Bts3d?Tn&B7MDaOY7Y?bDPo&WC_SWx?zDY4_{p^iV>820b zw6Czts95~=V$~f zg6O?yQJxa}2vHcFqqW7J?AyzhR5V8DaQ!A;a-mpql#Gf=@$cQi{zz*!?}imq$=I)+ zaSQ#2Tl*X0D#YKK4joL5EwWl3>YW` zIM{2u0=xu(BSWANGb0f(u{fNflCTO2DJUBHvkBLt<)V`+8BHr2`1RPw{rvSP2R`xz zKwbcWf*$uyp^F`1lTZFElgn?fNbwmPogX|lTdg`6@=lAhq)7*rOwrT{d8Dl@VdNm# zDH2ppFG|fy!@p{jT%@|%896Sf`PhzfHVdLH=$VhFn%zKyrbC!-SJZl;CW6QXL_?+3 z3^;JxWU~ofh+|bz(q}By6sH0qUa3Nt_TdmlBgds?tNd*`R`-YZD)tu`)9y1UA0ar( zhYna4)sQyC_*F*LB|3j>AD62s7=w${tx%tbt95SFRk6)$frc<`w#YZn8MfuG%8Wsn zW+2OxTef}q2syC85Yb;(maiL-CS4ypQ-n05oK#*3aTZJ#D6$#F>Fjk_q^@covf!e6 zjL`VOs|W3z+VzAsWLP|))im=#-af_bv^qA9C9?FJK(hTfuQ{9S$Q7c0AsV0x`^)nUejtWRVu^3G7X_3Wkkpgsz~` zr9h|MgS8Y44cI^I)Tc@(hlZkFW zr{Ix$;g8B0axqQbpYSE6Ve3LR2Y&#REiH#toWmyvjY;{u$||nO)aW^cEhM}@Cbch8 zQb~h4F1GOS2rfCW52{4;4Yk(sA$$>bD!^227x0d*jQ2W9U!>eQcjbtBTFGNQ3rG0I zGuu(ue5g>DpqL@ki6?dxTv4IKdz9k)k>8j1jj&Um9OT*5%rO=1R5v^N9L2}|6RK?G zy87(qR(D!M%d)Cd{Odt|g;ZHzTqjB27kFCB^E%X@ zg!)*HM!;SMl~Ad;AH3uCp7AP-_Z$pqEV|nIad~nrJD_a8Er3(bCdD^DjBnhhW_TQp znUj`wb4@^BLFF!D3sH1Bw9nV&1g93A~L96Q2_%$M_Xg=g^SmTH#uBd6vivqzATKl+hFA z{nZ7K!1JDzC&y?$^--CLH8vnFi`zFOrr~{w6bj;W-7Pc{E2tcyrE;JW=|}zw$)hR1 z_x7b#AfHPVQ$@(LXUhpZ?67?kNb&egW{#?aWecoE2#|$RlG^TX0!=?LqZxc2vA` zE!)T&dgvswd+fhlPa}dgCLGYYB<#8i#iLc-*noQ?$(uY1om{6USbX#$&ih#PnFRZZ zHKG;8da_+EhrAb3;bx{dTcf^+AwASZk(+Uvad8Wy>HEbCK+){FmV2o@CVa9;VsJw& zq1|1$QUYYi=>lPiR?9Zcc(Q`jUG)4LxU*lDdw;V@L21KPXf*|a$2*+z4r!zd^7@oe zTqSq}PmluH{402fV&>cswDl5_8E06IwY2N!g*FkpBjZLBO zbWbF?h06-+1@f#j1zb_inm=mp9z}0#O1$=zC#ymx_qAHx`n9sn`Q!fKpGf*(%x_wt z<9+k>YbWTjy&~FK)IEhq$G_Fk8ujKX-W@ic0 zqAXDm28TR*H`$AeBLIQCcCsH=Ho8CP;tYYSFs&RFhK;FBie?;e(JP>{dT)IF0zi83 zw<^IjM0K`klD%YR``l!(M9n({esM(DU)t=)efz5jQiI3!EI4Hwn9km-2g!|J&h;jS zo8ObKOb|HUM|3#zaeCK8n96?%m#PX%ke(ON1FFsrQ#mkk1%658hPPCu1WGFlWtOX> z(Vn~jNCbfW1CHPN2T;(Upuo}HU;P6lVE+KvJplF$41pa4VMPc3QwTJrIMP~lquY?v9p5Nts4G*YHFFm%|5({~`ufbxl`yuw z4aBqRgxG95+ed7zPMpmvO*xgI-Wjtqe~YP+a|uHyO=;g1wlcJ)Vdt5jbobhmd#eoy zGc#l`&8DV^V|f8sRHfaSR~J{sZpu3&)WDPIc{VdXY@uEd2wjx5TI%7*@hDgkr}kIp zHl(EcHmD2PG<*|>LhDsQqv59O`;lf1zM#~{msg59K3yvtlG_qoOiFl;;2dg((LNE? z-eYqaL`S9KDAiP5p7T|T-35K!@cJYtx>wKOv#0Q@4m99s`5mRok{#6>^TNGVLf)W^ z@jj6{546#y1wEWneIzY0BP+FSQgiz_pS-;>N0>p|{L-(ZXM4XkMd`DbZS>{eC>D4!G zPwFMhi%+1zl#+OxV&Z}B$6tM$uu@5jz;bDeD9RHw z?;L->PyVgbQ=?%{33^&Gr>N|G`EWIINrD`5=S0kv^GsTrFtW4Kw^YxYkf3r^_BHlc zlKrsR_nb-6r2ymBtXz{L!pm@Fk}J>7J7nVX zwuV9V63WLoq2a8gj<5-|c-A`A{Rq4p?52Ys7_~I2;Uj<|o!l=0^`WMTS$QA@Ojl{HgaWEqT zrYPLFmF0IBGIT?{(LnJ(sj-H{+{4~aTLUReC*e&4R2unX5 zlXf@4)H2NA{!XM4ce9SAs06|UO!2-fvX2?%cvW5_ZUZ$PnX!g0`Rvoh^I_IEB(k`7 zOyQyh3s`E3bji@iX(EN)7WMui-=WcPXV*S&JtG18?U9=5CUps)Vz`iKjO3uuvwm|G zEr&H0d-6W)_nQ))byp(KXtt#p?t<%Y3L);oTdZBI>QqM2v&dKs_x3mGimJ}OF}WTJ z+A^vO$BU9>_vSNF{61Wa+(Atgf%PP?E*U#V$&LyfjuC%}Z{N6r9u^fgg|Moy@o3vC zfKf%{ez+hk*3FY82O$Z74jPV&?|cC`+>bt+pNw$?#0gu&8kKiw)Q5t>`qp{5b!Yw zGt3(ETqNIZ>wW?3#sXU$G2m<>C@44#6f7hN7{nhf4l=O45duddB4J`tbm$ROP%?D% zk3%J86*kJPo&I@>#uiXFbG9zx7f+^aVBh=uBqO8%aFWqj`R%h&L?b`pEmVT~-p*Ho z3YUX79%s5P#;~o=AE3W!4t?6&$;&qg$oCO#)wvSXpdXf6^w^GYnQWtTY7c1%Pj{Sc zc4`Q=4o`OmbK{wQ*VRk$ehQ>1(;{Enb{yq{$4mfwH4i(jIro%!QlwB*~|m&$^wIcaQq&W)f_(^?iRh zEh<#U4A#6`yb-yUTgnBa2xXsLHJK8^jphvdYu;8=>nF870(->+WVX~O%eRfxCxh%| ziWET|`KXGi2UAn}xw;(^wtj`emK=c)^Yy9idYRxeCPwdIuENsb)}lj`j*+FzgvsVD z_Hd0%#*Sv-&}Y-UaX!0P$Q&Ya9#ERkim;_(InXpncWO0|$+_5~i;<^hzDZGTDS=6Y z*x5^9wy48(D*EWmuUfmU>L$9Yx6Ez5T6`T1-Ajm_mB(>)9NOeCVqBj~rIDtph@{Qo zS!^-&wyK8PI@8$)2Ug~iYW{$iL>OaLj!a0ZBqBx-bk>BpGeGckS{dg^jZ;4-TTb-! z=cu;66wB%J`OFe~q6slQN&jlY2A2q^6|m2XCoZv$@%FoCht<6$p6!~G8!pRtySzAl zExe9KuV_u{UR75suFdg&bLU`jRsw|;%JcB%x*gR3P$jZB103XU5o+6`g8Iu^RtDPE8kvH;-$MSKTs(iMD0-&Q(-3r2K4i5D>~1{K)G zFrMSy_AxeLn7v2CK74V?1WI?=0`edc>AW>(>#?}tf)o2&M+z%bp1~mS0x6Q7vEWB6 z*Y9}D5qi-c<#*>K`3e{*v{w+4_!{^0@TT@iNd9DzP^_5PW3Fe8Il2AQ3eD?}yrC1M z>J{GHh(FV~^JfUK`eL>lPnb|%nQV$Kyr0|y3hutNiiG(!U{kXP;u5@Tb&uvYOxWRb zvAJK*S+b-7U7d8wx4*kECEi}%qYR6sN!wjq=Ndpa>!g48V@YWO!KuTX!#13l+-g5U zL%1Zyujva_LmFxNo?_0{1W7)cjw-iCIigQYfTUsiiQ;K^vrK5W7{&|=2jbI!+{90h z2-!lq&n_xHlf9I32v@4-vSh7P&EodRo9Xh2AMV>oKC!VMJ_U=OCPHa-SU&s&`7$2c2DIZe%CaGhWJ@2)pODc?iL=cQX(-c9_aCJEWwdU!Eaehum4wGnF%sCyYQs-x z%2Z6nb2ttIK1ZX=s>3L$QqH9%x(;sR0;fo^^bnF~QgP(?MdFyXL)0Kr& zi1(fORriy$9s)UNH9=?-U9C)c^q^=(tCIO@_JsWdJd*Bj{O&2>Rn#&~&a26)MZbQp zyxCA14+p;$X;%JJ<%m1p9HByyposJgv%kP_Qb6k-;>DHLt6jp-2tR)trnZ4_-E3^$ ziszO`v+7I>t7}X55pj?d*3y-pGKdz$%hv1#P(?@T>E7EEW?ml&`{dhhWZl?{{0%F) zxv!%Nt0uU3df`M>ryLq``33MJde0gCH2A2v+t^WEDLd$wu@uZ5`*4C-XsrwyG}(I7 zt7x~k5+f)}Hj*DRSbreg`22*wQp%2^VzFr1Pq;Q{ZJJWz0C_ZQ!Z2&p^7{ES806vl zM61o=O_F>cUTVY?y|)3oLYy^0M|<^r7GAXgEnUh`%L`PvuhJ@2XYwJ}D~j>7iX0}( zg>knRyN~eVomPP+qqBmAsz>{I!B0!n_iATI7~cvKBE94ZoLh?qIj2nMrZA6n+?DSpURT5w9nXNFQ8@@Z@?XI zARfPEhElw9drye`1536d<-gpMFfr15&0`_38Hxspc=ZCSs$&&}S}BTYv>Lbh$yH~M zWqNB|fjUF_NF-1RXGIs*MLT2)`^L|i+0)2!SEW~8YNEj(wF<@EzN0CJRFG+azW@^D zp}lRNZU!J{#dwLLYbj0#NW76GfKRQv8@?C72o-(reyjOmFH@Og{=Q5MBUjq7!e_F1 zj0Q^cOHq$U@%Wh&<0H6g{)eu-zi zK_1e2N*;A}X72H6&Qz)21QLBbd|m)sfDCREFjL8 zpVwc$KBJ%0Tge@UIWig*)QdkH@Xl_^tE<1h065%e_%ECq@o{bSavnNancz7a!pU`A zUjW+jt^+;(Mc3Kw75YRyyL@wFVs~ue zKd2$V7@PI97Jy1$OAVVmWz%q5H=AlbCT)BoVOlrd8D&<5@aXoA^i%8;ptU!g_k(77 zivxRzdv%E^vFCD4kN!RAJm`$ST>(0tE3tR|^3BOb?8PF5h|tZ}7$?H#MIqwmEm^r) zf%SO0yTytG4KkRk>j^lg$x$^u&Xp_;vTx?Cl6(?4aTbynmJ`v(srhr&XD@&O0VoC^ z+Iq-UHh&s9nLDhT9YP(dHOfNPc9k|0N7XJ_tL3W7?4=>w%?_=TtlKw;8Ec!5MlqK_Ul*8-DOTE7i3cFkFM$o?nh|4m$E}D zJ3v-(Y)50-xF(%&Qe#o#v0>J>dB@wy-MvHiD@-2hTc*GF?}X7z(}ww4(Ot#jD}{Mz z&z~aQ=8a0Fcb5+F)6T*sKG~5wojkw0BtnjBPB+x4wP*K!D{_zf7_Kl+#YM?mC;>C6 z9IpJ_cVJ@Xe{-WK(s^IX`UuTZJRz&P%HpDi%lUP52$)st>I7gX0;I#B-DBkv!6I+H zZN$=(ncDX)T(YalSsBfiGiMI!-sh6r(LU#q-9eLa;z>`;`t$`R$J_Z7Ufa}_x4Csu zL?_4_HqtJANFkuX>>8+AyTkLEdUJE2>Zv&6?SZlM0+@N;1%N~N<=NenW3Wt3GcR%! z64_b6KEu8(CH=B$;(dqAeUDhlR$S%V^ZAWr%co#wpIrCb;f(Hq2X<=Ykm2xkp7nvn z^C2=W6_Jd6Ui=(2IBRX8foxxGTnw47ZqYR$HyOJ0b*P$s z%O@`cu{L~u3T-%2bV{0D#rA|f5^)_Kuni-Iy_SS*u*GdXHBH;6LlpR7H&^ZHG^)MS z7F^7{82;^hwqU<*L=)vVgXCRzXK_?pX^#Bi#G%w5)o_}jy_;_*QuQ|{0`AuM(&o?X z1rj<_D0sVBYEB%VpP;qY>eD{JJ=zW8F|F;|hCXz1#iAvet(a0$;4%MmS@vhLp0FINhX)YEn zsC^$Zjlc#%{P=^>nS*y1#4HDHWu-#iISX!zJR*Y=3|h}2{H)><+#|%p{7=Zi7l2Xv z;^G+_mF``1OZ=8_i|ix!^yJ4Lgw!d#LM$1W$k9T?$Q{H%9-B`_!-LKeJf`hD>6urc zbI=G3QHkbY;4${QO(?_^TnGcakRR^wMy!cpX8ipM5!YjHrGt|+`=aV@RT%r+DY>$q z!84ME@UhyDsq}21F1}RzEq32h$qg}u`)#A!L1%()C9LGMgD>Bj!*hZE@SDle@&_51 zj`zDA3Ts0HKhi(_L=3-ejg+BFMK}C01bhDHC`rF98&gz(xC?3`GU(KLjmm6XfiniKHKw}x|-H8sEj*DI#vsToK zL^5L{I_*|%i!oWPB8g&*v+g+!-8;}tH+Z)pUa*&+3ks?{2r

Tjvrz*TlOZi(ptWqWorM9SfUD59F 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 GkN?>K0>yy< literal 0 HcmV?d00001 diff --git a/assets/images/posts/download.jpeg b/assets/images/posts/download.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..13d1116cfa91a6cec978813f13edfbb9500df363 GIT binary patch literal 14817 zcmZX)bx>Sg@FqNiySonV1a}B-8Egg*?hF>(gS%^RcPB_7!QEXZkl>Iogy8xTSbqC` z`^VNkx9Xl#-S>2L-Re5k-A}*3Uv>Zl>dI=$03Z+m0KPtemrXz<022+Jn1q;&goK2g zoQ#5so`&)j42(21jIW?$V5DcHVc=wBU}mFdpl1?f;^5-pAKD**oosA$M2=l~=j#;aDE;B|w9@;^XDL3;fV07N20W&ofN@uQ;2=@2tox(kG* zm7>eH4w5iI7S9E(Ja)q`NIxs2modK{rvd{0Z`-fOkpU>EXsd{<%SFL5Vna z81@y^!&Yr=Js(Ob*6Je@_(*Gt$*hLNINbC+d^5V`iRVNiQ1l+ziT7gJgAHqS5+9Fs zfJP>}uBNZQ^1^9|^&6BP(@OOgWq%%Z-jCG#p#lkLnD+J(#p+o>q(e~i*}9gBmDOu> zZ&OpSw;zVp7}7FJXXYzAt!v^ud=l^Vc}2ceSdFPJ<;}`j^k2&70u=?vp(dvo9lXrB zMrH~_=Eg6CYt~iN{~CE)8Ukfd#pp3EQpM_o(v_nASOdF0kyJFCDAH;i!V1B(F_x#> zIJ&h>=3LOB7K|B8X|JuK`j`Pi&}?atB?$qY}HY{w)b zFn|V2A6UL(`R$Wuk_zuvmB+*xi!FO_Frf7;a+=pL*|>?cJ1}Mq&zSrDu;nSb?kRa9 zeHm8>4N|>>u`S8-`#9#}2ggUNF@pqXwNma1slyU#Wtbr0dCp$JB5QQ5FTW!Y8i=NrfS#G5g*4*89|Qm7#4*#MJqK(^g?+~&8m z!=koMbq%8PYobyd^FZ-aqomcuM3c$q+Lc5F_PF2@%nh`?HRe6RsBa0o4%3uwx+w($AW zn8OB++O+}Bl-O&+^-X6pOyTXwqXP3w*7H7%I?m}-0->!-NoDw_g16Wn-?L!3hNULk zDldSz`I@JSJ9;qT)RkDRVT}g6h{q7sj$tiR!(ar36=@ZQ0Ok+Na*8K1$fsqbL#e(9 z{fI^TI*MrI%j0%RJ~uIraj}r7@&_{t3s;e1vk2Uv?-`i69>}4HW=^YvthgJ-kABy` z8^H*O0syamTq~Q2-T8N)I3lxokS;G3z3M*fGixKv!c;9gIm~DC-#D2Qq*f`{$W}MU zax-KZ`I5#W{%d6>bExd)w8`X6OTV)Mow%}#V6}+q9Oh_D5@cqoNsi7CjZN$0U4g<2 zU`D@Gzo^t9imBhc_--hKXb*pnD;fb#G5zSP@lj}bZA6AQ){b9Fkm-)sag~cqDD6G~Sr3SjZzy_a zGNaHcHKajDE&1_U}!)Oja zPN68_F710S0J7cONd?sweKkfayebsM0E=`_G;@KtPo?T5jZei#_qr>Q%6J=7g)8BD z1H=vCC^$;9G}kBZpu0w=rKVW8(O z(V~WxtgP5|u)o$#(AS9DfyAqjH_zH3T_ZlO$pwBlo7bER20~AfNYzJnn@@9;50Gf9 zEZcgcchCrMv_yqKR#$&EPJkxEAst?aZeDUtxG=vY*btK_;LOP_LMo6;n+aN}&ChjU z%je>OQ8)pg4{n*#kU%LXYrYli*=vs9%eg#pky)}njk^aie#LjcfySRI9k<|Bf{{Ot zsLG63ugUcnf=3?Ke*Wc&|6(Jq-0nwFt6HviJ`9xj$Rj8-q*M(MG_qE1F}6T4M6M0x z2{PZKT(@H#!c2|{>AD_qEclY313?XMAExb0wFul#hPo_nV<>aZm8Vt~R2yP7s(qUB z#)QH{U;o9NU$RE6B-Ag9pK<3gMgC3FJGMMon;L!p6|wFF|gh{rBcS>vcXG*h;M)=@7J9 z%ea(TSYA#-_l~gjc5r@ZemHNGTv)%mT(fR!Ne}gB06!0d+z-PXX};`@vxP||fnvVH zyn0B@gb{%Q*(FD3R&po(Hl4r&v&2tZm|VmH5tBn)pHQ;n4(1LgBIPTqvW!nt4kB0jBQ=(cU5Xf=s~wS}ZSr4;9L*;?Z!^A3L0$ zU7vJ;_r-6kyq6)e1j}oz!<$viyFHM$#g!WtC1s4r@#r#4X0MDOd#t2f%>w~DK_2BK z_4c9eazCtKj8wI>tzS(j_FaFiMLz#^7(_iDM3F_?xg$`#7Au`D7^&vl_%W)0&C0Vt zxLmzV{*c1GYQZ7HV9NVfk&hbNym$62G5u23!>twfgGpyQTA{$6stEPwziqLbfj;5c z4uKAswi4$+oQMP%jGc-?;Qs4;I$p$_AIf78Pz{+I4E?A20zmLJ&KS4(BcgM)=k?Yw zW0xgg0DGbjR*PzWYLC(%TD>Pn2#u#b}exLc{Mf0}mmC9&w4 z73MkWrq4qfucZZ!1yA>M?F&}bKnRH02PVk~=_-QJcsysJpB6LA{$!4df536FvqNcj z0^%>jDG$|dsLEuu&pe0o?r$7M1=$I4BlT+Qt z1YX5BV~eb7cl^uvJQ%);KDB2d5Zsmy)orkikfWVNRmAHt5fdr zIO*lQ^H4^xTJaUJ?6mStd4H-O$8ZZ1zFQu7G4;KK!;Yh_2caIO%7?LPy@vm#3fW`M zxRT0+ovhTu$nX)4(Z1*Eux~~hpg6&f?GK(vW#}ENx1=d3BugaG%Mzc??O&U|cQ2uW z4ZHCvTWyuA(Y4*E3MX(AxmhOs2ocLde1oJmjE1>Pf{?Ua8pGNbV*m1c@1h_3GNkJ) zsl5Pr!h_a_PHY{a#`U{xxmc>J6~4$WqsebP^l1I_Y>>Pj{)jMH_bVszeSC8;yjW@Y zrC9KDWvp1Y)qCUP)d*aSnU|>?4+HIKHN;Mg7?-`SZ~*cvq@Y zIqgCax8{cIGGR`5d57)7SX#^ercs0(m;9TS&W6v> z;mE}#s}O57y6iLG3JfBDt=u7}?)nugqlNHV(wyM3NCWb9RjF6p!Z+f)`N#Y;&iAKV zqhTIJoUu!u-#9ZFgi%j1uNKkJa`t8VZxBK~`@pxa9;FF$qsKc@y z`Pz^yv*u5YbCF5~PD7v9OHaP_MtlfCY0Au zww(pwU3>w=E=gbHUiAD^s%g&xf6gY5bbAii?_#5yi7}|@pvn`2)=;Be77G3ju;h?i zO>%|(UcWNRTo%Si$iI2Mr+)GfYlvkaWeuFkPU-mfp>q5oR)|r|7kVC!cH3&joTAZc zkR{d)8|D2>;lOx$X!S7~_XW@=nAL|Ko?YPJ`}Booif5BO%z3^E4&><&sI?`_Sx?bUip*SGrK6K zmh04WbCU)bZ;MU1Jl}wpvCQH)2g8f`{p~hftb*}PTNcvSBJ*qguqwws`@6i!s@mdj z!-{Gj&GY?{ipDbKKE)=~;g_s6qQ8V#`56HKfUt2ium}Cs`gMiE@B9gtisrJ;nYvkw6x)8f`( zto9(GhS5^pZNcSd$Sz1~m$b~_DKDcCV2}h#@yU_Ca^f9`vU8v!+oo8)>2(8EwYYV*$pKu6kx(wrd=O10SyriMzqp#;do_8qGi3XTztDitq(sPW7yG zyPMY#?&kDb&*1E|pR87^ypt@a=XSJtn=`v>?~h(xg?{;ouIwS+1Av0?u>>N8+Z28I z4>_DB@B%H~4mGF)G85DRtWpeh4%&=;fLny5xI?}N)QIP0>6qu-8}2@`uSw)}R#O2l zr>ULUg7?ndS!l9Tj#3pIwBuT@RB>G<(H2|TIvMwW_;ds<*r{t^r5XuS*@S9lY`6sx z&e(Z!U$4arc4%?9fF?xv!1PeM$( zWou^ZM=+{&$gnsu;pX6*Q9AX#ESUZ?ZN8pF-Lrm|KE@1XNX5i)7c=F!F7saPH*tHE zpG`QL@77onC77v+>Ki7gljClRvpV-Vw1x~UMyJ`k0Ln8qL5=IEZ8SXjkxR8ow($4{ zv_DiT^XjSg3wLAwqk<%}+MHwz%Gn=SYz*ghwzeOCP2yI3cQ{z$WK9_KR}9aMM-1&N zmjFdp*Jlaa1OBsY7SFDvYOe`A2AQd*EMro_#H**4o&$9k_9_WZJ7VeF5;U_5d6+7r zWgz<_%_(2Mf{j*c{EHgNO||LYc0IQBUqwC74S^O;WE*>)mMLdK{E@j-f!7z0S@KL_3o( z-pSB7EJ%_{bxoHeA`uxTjY))ZE7o8-vOkYjX^p0pD=3^9A1M-Mhs8{R2snM*|AjTb zvf>nWkg;Mcw|u?2TzfO-`{@WPu8|-#SxD}wR~SiRaIAXD3(}8U(=NM4as4wBspOVc z(k-I?1gkT>MTKJT0K#f-w*JHWdD}$nP)e$ovP$Ziz?|SOFx+~{^+Qs6038n#xl0)LGr zJ}eCF7`;uYV*Ry+cC(hGwAP;q)tZ_8fWde)qRc}2Uc}2L9ma1~VQ^&TG_g9lGVPyh zWN8RcF7=j-=%E}l&DvY}oi0UUp}18_w;2&msi|7mL2*I;jFtKpp=%2MRko}r<7AR% ziA?B@Gj01`6DrnmESi(10H(%ZERmK7_ZcMy={An**5l(byJjiAW<`l|17x4uIvC|C zAsDMoo2ZDNHZH*}P_}{l6auWh^gI{^*ReXxP8zK-dLZm(@b*bC1PH8)Gk{W6v@d~j8*tqotfoLsKO8E`n;xcA>R zvYm!7l_5+2Va`b4R;|_ehM@01jibu3M7QI`Z|#%`Wc5FI0W{;wDZRlQ7qf*jY4P=w z>cxAhb$hoVQl{`u$&Pz089cjt{wY`L7b4+`##E?=Dmr#cj2!d-;K;WOkyaDvAj^ni zu^?JLpNW-LG?UPz64&UN(0;Zu#E)ilNr8C2qaEbsQY4XFWA}KAr$2K~DyQ#VuaTM5 z^Xim)aEYS<@y|KK>zJ4jJ zLx*`X#7X3Yy1rSmx^Ke2WXcxlWyzq9BPVPi7KEVdtS9Z zG6vibZv(HO`#*1B4Al!ig3HN1KM9l{>2QRJM;tH@q%vppM*YH9|MP3?Bx0TKBga}p zAD&rE_zeCP7hv`T^SrS+7{Y&&))fJrT>r?TAAh(SzeLc-`kAG@3|Nz|xHSSdr_5G} z0~%6=QZOt0>A$;`^Vvp2{8&hrE zJ!j`YWlkuiNq>>1RY})MD>#t%;4$NJO#f%AZ8`_!_iFJkiJPHMP}O@IRLB%3@LOgM zQBe7kmJSvTS;pH~s(}{b&fDVB?vWqL^?EfOA`G$o@JS|uzJNmFk|eqQ6~GVV|NdB0 zemf6;s$5o>InopOvjQhp&KDR_$uzdw=C&0N`lnRCkH%V3+Z33);`2M;kp}jIvG5J{ zAWoykLZ&pOmKJmw)1+{iL(jN~1!$Btg?LlKPUxY)k+JnLhag?8&@;hH#Qwgi?VXY= zF&_|1Ze?l-ci{4;+GgWvtXRQ%g(DfU)*LT`YP*)k9v^`slm(SvIiIM;7$w{C$jTTg z%~6z%*yAVvuJ{T~5AzwoZ@^@fLBCODdeR%E zSDFQkB+DgPRNcRcD^)o?|EuPb*vQQt3GhgtdNV5Zlha0wB+LgMI#xpaNh=4ecAAtz z$xYg*az|T44E^|zuXYJFG(g?CR}hOt=DFga6+Fcf3Daua&llX z10d&`891or?qaw@j%(A9dL3_A8Ap5JT(uM30f04duNY-=jdILIOTWmHu1?Z-MUHvy z#G7?9!jhuQcx>UD3#Xmqc`KSeFZ%D-64u_+q4`NzeHw-A{CjBMXvx@ZAG^DE{T^9e zV&aU(N?gC%uqa^K4-+atlWj-0xYW3wPeF~+bi#-S zobdsz>H>SloOtQ*F%eY(I$t-JWk^u*7X%Lvm_u^^YteKgCkq;2(1y9mFV;*@Yf@f5 z$rnfKvKKP)F3JAYC?}Zs`P~HH=1)r4`9{ZU;fnT+>KZ|M8FoZ~l|#VhFS$t-gH)f> zC|hMITZ5K9GwnE~G9 zqo8i9DT4uTR|d^UCizfCNv|_${;PzGf?k#t!+cmIYNGE_Nlq~su#eGPY#V_UirL~3 z;MRisrMZf6?y|Moh;z9KuY@Dh2{V`AAV-h5|2DgD6ceqWHtmC}OGQSh9mL9{$z+eI3=B;s9!1%PNEj_rpP|KyYe8_9+P@l4NX z=KM;P=Q~KFB9*~V2wtX18@r>mNOwx)djdN@4JzHWUQVa?+z9fA2Fwb%z6?$_RZ*Ej zoYWu{)=Bemdsv%2JazNErDeE@VCYd>dlK_(`68}RM>L%y%LpMCvilJ^VYHTOnTyl! z+n%w+>-4xhMWwGQnrq0GvG>v5u!$C(FYYJO7w>cYcyJ0)1H9|fL;dpRN-l_WU8`Hj z9=4J1WsIhUWjX!0@ly9t?#MDWe_b1WH)LnIZol`Vuxk>4?|59q6MgWOamE9W=KZci=gRq^kbKZSo0D?6fr)}d0xTM7^OP*XyW=q%jG zAFSuOm&ElN20Ntry?JUJ`I&+39K}V_?mOKx!$-jLy17>EC$u z1RajQ{eGY8m&gp^Ptdj+4&gfV$g53B+qLEzTcs}qxa8C!`atrK9G(wAKF2;mWcFe6VZVZ#dF#7taT<|YA8fh7xD51ND@m+BlLMexnAxMq` znwlMi_Ncx#b%8EiL|WOe4ec0|WO??_g-GRmf;Dh2bX7)h#`Q39pqd^s!sS}5YRp;U z1yJ0F;cgOEsX=Yr*_~SDb^JRRJO{5G7MRG^|1)|1K# zlD$jhxT42{QP#C@IFHo6%{4~YH>RA#vyYXh5XL10OW+7yv0<9;I)0t2v#u4aC~Su3 zerY0 zd0hYjkU(*)9w2cVD)mVZaY$4yi)We%U}pVHszwb5vXu;IS1O3VZ;h_XssO73HXl>m zJgiSBO0&=sqhi8QSCz{Y;Wfo^S%G-)PYIIoAZP_=Ygol8^IZ8U@@bej)ls>Cut=$K z9vMicqMMvN3?(`YZuvKEdIQIu<(V7(}0 zQ@7cW#c^yS5Fx4WaLw_>)t+cCBJZH_fV1!R^MzHkZ< zreJFVe0NiPFt6M%tVjLaBAx6fYE;bGapDUg`2zg6x?KLNPQAt_^T?fuZiZrw@)BC# z!Sp0A6UQc2K0NKxoh)@XO}2OOc`J_ZVJmgA$yAdKj34N&^)ZhvG+wR{xcSFdHI<&6QQ|9}yg zY`X>hpltPcXT>bes}$7M#~4+T#Sx1`yW74Qe#;(R)xMg#>_R0A-_}`u0c5Dj z+;dD6eACPV>X+n2B@hg#XPi%*Rba4Huak9G3(FVs>$mwi&yeNnaUXussZ=bwh#wQ$ zB!V0gz0%w`(R4=9S-HsW{ZKNo=A3=uh};IDq_JYP1R9Le886OSbBcQ6BRyzcAi)43 zn4b-UX*lm06PDKM0Vi8?d=eP0N{*kEA?6Hpr9(_}=|{xF~E%PY}i0KUSVc`Miwy>GD^IFV=JXzFKnE+6U{ps z_JguZX4Bd_>#|2gb?N!t_m2#y@wZ>PneGbA4iRti8gVqUh%8GUm4P1~o##q@(>{j( zS0H^J(4ICFZ%Ofe3#5t!w%*!}eLXDw=QV=ELmgzXQTPwVN`2x1$ zE$ek$O%=BlG!UChZ$k7dKDOHr##i|iNgchgD0qS%I5?_FYA{@`Q5qX6i}Frm@(-^S zpGeKMeey4dZ&|w!7N(AzY5z1!GNgaqc4FAxQxLR)W9~b6GQDY))(on_xRrUq%)ddK z%?c|!*`~>l)5n%X&OWDswAEcBKQ=kal+5gQx%-bZjOMbmW8VRnkY+~Q;{A<_lt|HY zS#<`pL|N4N+F>c#)PCN=0iQ9JHQtFKO_LMSeK})7?rx62$|7o6P$;pGcYN~#;M!X3 zqrDZGROS@e=@{Fq1f2lJBja^RfwA{C4;Z2-);fn$DG}*qpE|_*8@?bk$v#VUtACBx zY-5|Mv#32|av1&>fK&*Ium!5Ld^wF7_qHoE>>U%lIp!f(x2vR8)L&p3XrXd_`>{*& znWzgA#fj4!Fq3Hi5T&Mt!*4C&3zsAAZLK(1);Eo@6GNUe;j`mlqD)obKnbUnBR*L% z@1u(A+Qhk7s?BH!xCf(mQ-uWCWx`>MB8k+EV|i8jm7K*0jQ$(mt1}Bc^3OMzH9SNX zPmOQ+(SJqF!g1q1hAcXtkCSG1gO+S-V0Z)2CE%GDWcqwcy&Zv_j%d5#xKs4DCLq3vZHIAHA3xfssK5)+TtwOr`z(VUpyV0zAY(I3t)g`Qq*$ ze||GJdt<2@NkO4v4j^TUt#*4OVE1b=$y8~rW*Gf+7>3&IstECM!oHgF3jiYMGWk5J zHG0{W^n<6Ri@)?Lqz7EMzYXH1p0Fp}$~94}S` zas$S3A0AE;r+7$GmBqJKwtruKGhb6^?$b!L)*QZvF(`Xe8;91hidL$8Gj6y>H3npYqX)D7l5xkuI2#R zbR?27G@d!&gj7ELl#7x#Aw=PiBm9HDPIL<8L_A!-`80(y<`FMhs5oJl0(zgWdVC!5 zeI{CuHsgLOwIY-w3Iia?em;Y*CI&fFtawOY{i)ls5dRpSWTtHzo`r;4-R#iUJn{m- zzYP1E6_=3dQ7;qj%=g$S|L(3V2IHV1*>G716+EhMYRxw%kIHn(NkC;|IJbAtH1qw( zk&(i(MW*3E{UJ`P_SN3t$YzKk!FBzUuH>eox@cqIG8^ z3tG7Ykwt6+^59om~AmwjD7&G>LbPaijryK1ULRlT55Kw{)0&d?8?Q8A- zTKsI#(5e;77U@tI$~=is>7(!Bp=t7Dhv|plbud;|s{?s0pU_zg~Xwr>Z zd>@L?-%>VB9^WGt4VCRUm&_Nmo(Bu^UE~hRltgsIyp#PAuxo2yvGMEV-$i*cyWqs8 zP!r~yZpN5;`{u*UyU#+jWhipUzhX9wW}57IKhe>KPc9i;h6!4Ok}e)@J8dSDdp{gc z_NdoPe{M+UB$k=3m3;xs{pUUGj_r~!ia)hff*x5y#>t$^_ZkdxY-D0d@2y35XmqwE zKULSfl8|*Rtec-30&$rRV*+jt=&x$V9)?3$ns8nKQ|{0Ek9%a{9UR5eM)S(C_``|It?oY#ATNh*W}z>7un-hNkLr3}{P-da=3#95w5>`ML3q z7O8z$Rr;{c`-Hf3>NUUK7ts&}WWLS+X)*sEc^%6Hnp@N#O{-@?#29eLwn^`gE;gIn zK*yk4N*%$h(i8SpO8w1c<<6CPQonMZRQvRMh(zYnkmk87keUKSc|8;pof%d&0`G`a zL@?JIpRjIphAqJZ=G=n?YGgswX^w=J?Ptr*c0KQ98tqZ3Ec0dA^sfM+zPV|)qcqmL zTp7m}=@qIBM$Ssr@D%vo7u4NZ%?{(Ru&NzLS~=R+Q3hUjO<_3)z4HlB5(o)nZH+L{ zR|bwgaNJtD#{+F3<{Zfax%>fBKkn+D2s?K zR56hGyIWI%+PK)`0w|Hx))*R~tX^xD6cZ!;^&9=Wj4az26!M?&O(KfmP%T8$a4);8u|W5Zm) zB8Xpyv72!`1`7>1+}X^Xm!cv$DDqD;{zkk*Yh~3)yWo^uQY>A@yeKr)59?nXlD}!v z`QZ!MqoNph8+#imr=FU>Bc)bdvKcq(L$?!9&NeryGc(=|4-Z5IA>X`}*}oM3ng`J3 z<9-wA;dtIaV*Hya{$MR333TeA^sDCjjG{MdN##{^f*k-Be4QH# z=)8ThRg1EAU0Xv~7y3zXpP!4Wy2xagws(b-E-MBDx}oUm4cXagtVj1QFo9Au;fIzy z&*f+`+lWF_D9GoS;$e`GuF%-I%&QGrl#IH+9g;TGwT-QWAe?s>qO^0N8VuFVM+!*r z5`C~FmV1knues^_5U|q-3s_hCiN6{0+#8HX)9o8;i;7;gvI0hMryJWhnE3Pw>DMe} zkJ%;|$q7im4Km&D;a;kH)awz?a^Ob+YgKJ?<5V5ETn%T)WcFAI@Jq|0zyH^YNH1mn zM*r#GPEOB}W1ZHBf!bSD>Db?afdr;)t64RlIdyc(HM6kdZOj13P_6tW$-O2Oj%<_9 zeDXKyNZ4C=duW@66a_fDQgW-XZx}k*ghzsY{j$v0J#LXc8>Oqvn`8p3vZ*?TX@K^J z;F7SL0@K~^bt{Y6xu8~Z6t7&S(;%gBdB0WMw_(YZw@%NW`DD~?hB070=}V?RfO-aU z(K$E@auGyU!M||#dRq%U3%pd@BmE|%rv1PDw>V#i9~vg~6n=*|(;YUP zN_d6ATULI~oL7m|W(3Wi94bE}mhi<6k#L*o_y2PwKj%;;> z7MP1F;$wQt@|3**SXf27Hn)jAj%*0_?ihLjFM#c{sBPS+%|L<=(6E&y&VVfBzac>q z=NXREQIB-K?pDH+=vJ>qgdyATJ*lYu`Oo7lsuD`*mfZ8b+0M;my{^~s&2TX$Qm)E# zE$(B8GYo9AL3w$-KBOJWzKncFZ-R0p%9x~>GKFyy!GDcTF3{bGQo8ZsO`fNmcB_nG z0+Pm`*N_63PhUt=D<0E1bQRuix}EQ2n>6ZuBXKmU?dq9eb!NP{87h*`(DSGV$WO8Y zjy+$8>k@m}a1?UUmXR%2ya!zTNQz6%toDR15dtesid2cdY$Yy=p$?Y;#RoghXkQDQay^>!0k99h*?QQmfrLY zJ-0cl6z);hxs*EQSzx0Yp|1tIBfjt*;*u@s)+Lz{eQylpNxqlg>sc+ZUu~@AKvw5y zF)Mn$*YB5w!v%-?#D1v+?!M@E&6+s*w+itO|DzsKgYcyVK# zp`S8_S88F!=9eFEZU0;VVxNYj_n(s~?u|Kr7%%qAZDUiiu1!ia7Y7mTiC<}^>fI+* zQ$5nhv=t%Mvx|ssdj=cF)coUPN;o97T;8>kqhC@1;~Pg(=HCr+j|SM~1Hd_azD2JT zE&)<2{7*141yJ3@st%+C0)Qwo?gn{@jfyffkLuC`;ji&iJ2;20&ry73{kCKPi{k?3 zi^Gc(s$v1QiV_LOw@$c;xlAcn{qF@3ghv6Qc`&Yv_x8H6yrsWOnA^z>wZ9$ugtccz z63YkPyh9sz(+Z-Ry7=n;6+W;1F?u@)i&izSTOwvo1}>OZH^uZiwU4(QxPu3)71%L! zmQ3!-OZIa_yS+~7?&NO}EvPe6ZI!+#ypSU`!>#)GWtjG(NQ=dFUw(1Z@LAN%k;RG~ zn}^FWJSeI)^pRqR(zafcN~*z1#ZOFFGaP~v##TCX$Gx^(<2z`w3eg`&K~2YGFZX=A zc{loFRf`J~6#F3e0%$;)Jc9GRRtEoBtrP5f#lynYItz2W``z3_iJD0!Pkb0{1~=A` zLgt+TLDQU>v3~>pvuJd+^@Wt>pmEEkOCQJ$p!*BBYVygzowE8wWg_r0In@bIoS8iq zlk7L|hI!3;G%+I@b;VH5LZy}qo;5QTUQ-!klhGFB+#;Rc!9k$heW)JLYS<&f)(L1mq_b@mR(81uc zPOJWnurlWu*>TW9GhwCf*u`87B{^>GONqy+7VO++sz4l-)i)<{gwdG$Ab eRRYI@XWuz^+?t{dlC}(tw91yGO|*Xb{=Wdh`kF)l literal 0 HcmV?d00001 diff --git a/assets/images/posts/noimage.png b/assets/images/posts/noimage.png new file mode 100644 index 0000000000000000000000000000000000000000..a46ea2b6a1806b003ebee0b6bf651b9a0209b97e GIT binary patch literal 3167 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 eO zW%=)dB>!xz2VYWo$QyXPbbjUGZRutMczb*Eynf^04!3l%;c<4e%{UOJ1^`AtNlseF zCv$(%)j4(soI)621E&y?)mhA+hknCh3{qH(U2{%LOBnX_;z`(bpE3?b8c)Pfco zSn=n5vjL!`hmQX710|!BA2!tK)mYK6FO2!E)T@si-UnHq#XETW2e}EM$fk=0e}p@w zqCv|+cP4#0I}(E+@`N^K@$-^}Yhk)m<(oLY;weY(?s;1AIV*RC=d&j7{bFP-EbxGx zL4qC;ivR}_KmhGq9qri2vH25tIi?govK73A*du)F7_rGh86Q!__57%<6{wjAwq>Gc z^#%&ur#o83_|rLTMN9%ptGoCso4XSAOr14K`HbMIYU|AUqD4eTz z+hgmGN3(cOn#4$0(n=mWk=0$Zz3oNX%pZqXwRjcI*m^B_-IR&%)W)A;PI5Cf!qHcv z@jImvz65Zt`AxU|*N%=0FTC--M396Grj*0@N{jmZoFYc~+iH76pH{UhIe23hSGSP< zLSKo0&F zDd&4c?h&gl0a+hEx|tk=FwwDuFkYRU`1ev#I}#@A7~3L~GoE+t{KVq?MIvR)gSg@W zaztq3hnqcj^?1Cc5~D~$qUe2v928&21sbZqUeDb7h#6Pq6y)JY>!wT{Mw*EQ%>?G$ zATyAam(oO%YO1Q`iDJ2m(m0Hoer+zggPCF~W!!|;zk>LI}%1G^=o2%N$NNO&1)$|j!s>v!>%hTNcQ3U5{q2rZ8N#35>t*%y>m&kITNU%cjc%bZJ z7-E!83QKZmNcV8g$^6Xe)jpkh$~5MF;O*vm(ift1_oLH9BWAfLn$E76yB@@G{9aJ_ z-jk%h)0Qp^AN8`yy9|DXKu$t-NMp5BPMKG?Kjx{zT!KWGKKQ9Af*zE|=^C-jq~TF+jcf+;+bY0|!AT zQunep_%zFxmDa7YKW4q1x?eoua|)l@_jYG1SSh;|SmBO(C7gxe3hVldiM%-~C}2)n zfb@MCScJGBX0x2F%dh^VlJ?ebQd&FQ0DP!SX1TJ3d(uIw-77IBYBKnqnKV&6CEmLZ zS47K#!x7IJjn{N~Yz!OEtk&&y%5lW=GlJRs13HPlm;^nGImHWaEIE%wD!B9H94;lfbMzRlN{$c5UMhbNnw zy0*Q`LbP2|w6aK=hN#-NQcQgAh@46Lx+j$IdV~<6GjR?OtFVp^&;Xh1L+Ab{yRP$d z;*a!=10<(6EUC*|;Ou^{CnXPQzV0_Ea2W-ov#GV;BDB%|vXgc?8C3|%9!Q09Sdjuq z6F%y=3%;IgjFHzd9?imLR|mq`*Bbn29DT`|xj!5l@Ot*#(19WaKh@3I|It{rU#=Iz1psucNJCw1N(Bi$&JVRC8+PSCi<(Xm@JJJ@f@wKvs zc;I44Zu>CI$g-!xf#9)N2sPR#p!NI1PJE+^dZ`1cU*xVhopu5|z&G*`ecrMA48yRN zWl`!+9F0G<;YU+>mXxDxdZ+N<8wVEhj+_Jq2MY>TWx%Wq9=ljLYA5+YjEG-e747TS zuTe4Y-y023wk&1rb9+&o(!_jwrQ-5LO}-5M55?)vJgSmB+N!0!Uu%TNN83EEMR+gK z{^G;;7JGVn#9qF9nXwTsk4KycsIrbTBCaR^1^=4c)nnF$W>&%`o3Z5ObTKGZ)q*(X zobY|Gna!iA^{F~v_VUwPNFm!N;N~O#igJZs2Cp~cTU->n#naC`o0-7?uk+RNT?tx3 zfDRC|NVf5NPDLa7$9&8?%O4@=ITxn4%i86a-_flIO-h?nS?RC3yJ_fN#L(~5{cWD! zAYdV~6nm8BjsP7B%RC|bYbiCJl4)c}4Ff*u7$?4*`=gI`c@QFf+CvCDAAdt~n`SCU zOX*){caTSOt^P;k^Y)pgp}W-isLP$f@zUD^-MXGCNO^cdaHKEkM#~xp`a6~0>kB^@ zvy5~Ivz!aT!1Vku?}Ass<)6pA-brG#!QrN8m&o}hxO)sWVF}sZ)8KP&p<6vpB%!*T zcU*8-_?DXDYs2Z(aO(N{^WyKrLKGBBbvJ}=y=!IPl!4&jz-2Oq8|`*)9iz@FztD!ZXO$HLS-FKAo zs#BKtH)SpK=zBe-)r)@vz=vK2h>aO8oUmyNF@6%!(0h`;bT`b#5P#yChZfU9S!vj5Nh@Nz5n~yTL~+^)`rV`-00tqUIJ|`+wFM{hyEG|nxdp*I2H#< z9YI$J4Ys?f^RwxN8fp?`XS360T0Pf8tN{dVX5IP+oTRhQGana&{K_X5Lk4*WlZ3*F9_4skz*AUFzdVHYf7CDyqK@HG{)%c%G zQ`Mc!FLf91Or(w6pNgHP`-$Bh_e+akIuAeaFWuQ|+$uB?ztMt}JJo*%7ec69!sPHa zO+wJ3dMZu1!0fk`h%lcWSU3Ihe(IEg>+CZTgQgO6_33S=jet0%0NY!uf!4LM!&z2u z+U#&^w8n#`<$9-umlDXm;rVu46w7e9lFG9qCq1X)-gR1onnK#t^4~qxxX*yxNQJl) z%qvO8UIr)Udm-DYwVV5aUkOcF6#FjC=`BxdauAZbITms`26)@@iy(?aGKD+t;|X+2cJ4@${)061i$Goxx2m5?N|@bxC?wi@JPsFY;5o);aov;R<;=Q zYT`lvuDL|i9A=9K>Tsy4ccbw0eS{Y?sNIwu=@GwP0KlKrxC1aJe9bJnDvPV$}oE`z>epo*Fd^B z*A*i}YV4PftE<0Tw|lAN^_Nj~3$C?fPvHZVhIPm+(B|Ss$y>ZV;;ekjMVyHgVcR~v*!MNL!6?wzG}q~3M8L-}zY zGm~`Y2|d;CXsWmg3a;Au75oXX&edc*FS_C_>Nq>Sb&F*3r&w|3-8Vj-IwzgDRGJ2- z$4%Gt1=;fP@1J0)16rfMX;15La9PmQ)(2ZZ9;n-@pjdfMP(z;G=(>I%Riq=%N{%x! zezCE5>qN9h_VkUScEAY58UA~hxQxr*$&p`kEF(a_@v<4<1fv ziJv&bn>s>jsJ>P1Vm3~b6KxLkum)@a>G(8Cj&SjWC!!{x)O2y7EG9wRR6HxjkW5 zLc8Q>@w!MP-TH%Qz#E)Rvi|YqW2JJ}91G_TriW8}e)ni;d8T=xu>J^X=YEw<>#=OT zrRKNcXRHzgw=FrJ;XG7H;WLap2#$9gKkG)a&U&ntXZJDOxgA$pWalThgl6#DLnz{K_dmPa>I{4>_AJ`W}Y*#^8hM5oKFu!iePq1>|dv$(;llcgb(stad+9A0#8$i`mu z)wz^wn4m$gvx9*>*Sn(Ts*mp5jn;ByGBq?VT_5B;R6Jle-gm#Ojj+QzgI2y3U0$T; zPLkm%4>TSQ#qPEvzGe}}!du2i#qKZdFpA7O=A&wc&;oqOetx%$=HIZPBbFjHGa?R>AZI@ylH=RDRsv_AA>K|m(0UtV z%#xO35z&7NL#4y3IyhN$JHC~iRJ5`Ap@Cp40nMl1%3Hdl8rz}X$L!A!Cn-Dw;m2eH z6sJixCIM};(WqsycX9r$4mTB_aNn_{7O!^szWy%WRaM!rj=+C$B&Z){wCY*QrO!t=)4TEx4S9=@z3aA{yG`>)73W3j9aVP$(%$0t z({H0LY^OYaH&%{j5rj7B**~r^{(PO3rw&A^1UPI3mt!_4AxGXy3nA^oRzA<6w`tDwQH6(L`%>cj{R)C&=(arw)hl zoVW+Jsz}7PrM~ZQ9j2$Dufvb>5JsMWP_tYre$^XoC9{SOYc~bU%`KRP?d#v(t~;`! zv?uF7(!>0EEYc!H9%g;%lm11AxWYv=Et~pwkcRYG8;!{3f3(Tk8R%h=F8ONAv@dk(R;*DyUN|81?C2w$zy8gt%7%xv37NPPzKBO_fD&Z(1qc&Ba29 zq=~XuGEtK$w{sq>B9Ml;Agb75kW2> zF9sypfc#OP$01)Tf>W+bH!2yj^q0b^H0V3ixi{^c4$o6ukM$)FgEDW#qo?s*F5LUs z%X=qm3@;0Y0v@mTB##Hz=$)NlZ%YQg|Mf3Hd81%-0#-ZUxg3X|XCF0v8s-HQ%+>^UF8)Ww+1Mn#%+A-+|phint1EZzulC0r|vYD}FaUO;F4WMu%$k zTP$t<_LIWixdIqmwh3yYx=Uqd>_=yXq#55C^+7d9Q5SEnzMcEjq{P)rq^ zvwk{sYJ-(S(s`G6?f-TQOK&{ICW~52$T?3ye|)O!eCftd)hvD6vztEGrvEU-ZO|w& z?j~tS-!vF&^Ro1;v`wPHxAgB6?V&Awz4}$#TGO)Y54*6Zc+(QhACF5q!=`aovY%7w z+mb!cEUjJ?6|o(fax+<7g`}qC>7o@SOssYf*))3C2)-}7&1lFmoCx{Xnc&9u(c?k0 z!vy6tFa33#e51W5k9zt|M#Fu_PgYf<^you^iFY1_aHbX&Jj|a4&O+zSv+DO9=D77p zYE%pA4Pu6wR&;5@eNn9s2?+VtRS3~YoO+%Ap=jqGc(V^n`VBH*OtX)0<&9)y zWQsRyTX)bN>7?sxt+O#Ze$7%Z*7pXoSpQ9xj?HQFM?R|IL^z9ELr=QYNu}fy22n9( z+?HocO3-lRN6}SkAsi)HkTTdn2<`|#XvLR2s)Uaekq8y-O@c04G(j*b5Ty1V z@PRx^Rg2Fj!E&RqRveCz9u9tA1G`GBKYcgR9N(un8HqWlr6Oz4#Z-WYKb-b_cNwWl z6xiOs10i)fa<@Adb5p3V|utP&&wwY$fEygG(2+%loxc+;xnvy$n* zhuNqGJb(T?kP?n;xbr<1QHs)1>CkETZ;km()oxsozO*(hou~M>4subS$3MRQPDnhv zQCiB`P?fe&Z(pb_Qnl~>A6&xQf72;06xSB1&}@Wp=QcgF$bun*#`smIbYL-P)iQF{ zHkfFqVLZDs{R@3VvG%GCBrU4kGy`*i89+3pyG!2g4I^XKyo_U?Jz@s;VJt$zo2_`l zpbE5L`~#}6FZA@6T0$XFj+n?+Q~6mhZg7pMK}ITrJgs8UUcT_*>E$ktH@m6}d`@XE z>bEJ>9*lN9BEu*rj-!x&6j}pw)K^I37mV~b2tu`s1oC}PaDF@)KWzktsAR2%qsCA^ z6IhAZ$F;x=e~(GuMbVqP#H;O@O*k|WPWCDwNo#$wUW*eVq`v*Ok$t&G#l+jvJ7Jk0 zldGeM;d>w#^GDN#C>9+;n#?u~!DAynNuclVA z8xpQ`T#H*2;+{9)r<{FR@9ogDIznC9)rz()6+0^nO!3lk&da_)ysew>uRg6|G6?+o z$m8;1j`HtBz9L%55%E7GThFdDZJ9&fy?aMb5Sh<3LTb3j^GAr>a(~LLs`ADb{vXuR zFc~)=Q<6A)#YP_dAwk32(@3$K`+xkKkBeD)K@~&U?tbHLMgHZTRK+k#8|{y1nl@yA zAOU~R6?ukHk&|_V*SYF~Zo)rvy_bKL@QkA2087;1YjcZ$lf%rlW7m zu4(JF*6s~uP)6&rfYx4)Y&zGAC7bX?X-#&%<(dzNVz4^e{6sQ$j9luQ$M*d7)l#_)zq= zopS66ISn}hnGND5p$YRQ@K&p1_Y`Gi(&1BXF8t_$Ke}Y|;XvEuWp7Kbg}s|cdo{%$ zDyiuoR{3B2k=@}3)2hzEoCjoWeSJ8BFm8F`|4m@oX7Q%Sa+Dgj?dOw(alRbXQhzpF+ziB4p9F}Ev>np^>ByoQi?K&1auqJK*O0&xP6jC9@Boph%w&f= z9kf?*N6v3mMcA-dy31TviSFOMjJz&jC6V;8*)KUBM+00Cy1=_Dw-4Sx7kp`gJ&C=I|e?PiYrqH7S1Z#xj#zjJ_=tijUj~%`OCz-;pADm^j zKyeFTi$`$DQswKj4ccmSdCeLmu@=d6V4jBL*>~U3 zU!HxYw;j86bzy2|QEr)=&8+T=(r#kFJKbEeS2Yg;Gy=3@KWo^m8$_Upqf=u?p>sd^ zv5zA1=Y)xcZ;4z07eAkEkc-J*7`chR@u?sK$L4JioOlm$&bwUL{?NtytbWGS=Q1D7 zq)wC~nfvfOK`)K}q~w8*t2>p6Ek3m-HeRao0=^4{_ikBN{oy&`e{_y&`K-~>8&o4Z z1`jov*~9D)BO9WI5`b_PaGg47xj#&`FzZbyDV^c`XP|EO}2Z09y!# zG9EUbqMf1?^_iL0%bJY_!-v(k7bljlImDs5kb$X_v~=3W?X#Z-wReh3Q6LRKBKsd&3srWP zsv>P8s~~adOJlzA7>NQQRsiSpWdFg2!L{LNNlC{9J|kqER!uGCD0Eqk@YQptytXI{+czAi%(#73>xx2|A8E&07Br@>6Q zzoJaqqAP*%sX|ZsbK`yrOh2yV(565Y#uN)W814G2@+-A>vf=Av9xIhk&MP}wStl~+ z+PZ}e_-M5F5*XrL-3he7EoIK6=-rY`Sofr>nv;y8b!pWhRKUPwOdOXjJp7T_@+Yo| z&-(|{6HMW90^mRPG$)=?0cl134G)Ef1Z=5ZdU#M~0byGEt^=3RMs1m9#?+xNM2gY&k6i(Q{{#-lT6@-GG3aQXAqI#l!C=^3tB`n)>@B&TrohVxyT*@{Hu@Qig5VP z!X$QVHR*qLv;WJ0W6Qf?$zUha!^<^6x*`Jj$`1PeP(x9XoqP-=N`cai_;*B1bUiaT zJ!yq?jCNnjM~P&ZH{F`v1G#gLUtNOg0)l^9faj1$edIV$If97JZS)VkaK><=Z8Crx zCxrjJrL~$!rr;Olz`y-#e0wlah*l=*6T0+o$Ve+YHkc%P`k5M#>!T)v%Hfj7DUc3R zy4QO_XaRyg+DA}1>ZVwJHkWHCBPD$HxLX!GaipX^h!X^$+ajQ+0Pscl{zOKxkr%s9^Lx2&CLpCv4)&Ns>>d2OYESQX z(m8c2p7*SWjPIJ3p7b=*DJWv&sO_Be1`SK124`oz%;GL|32FWUc@u9zZ*e5+aCh!X z2>ohGrXL657B6X9-1<5P-{&)hD?;t(e6MbU{enLT&HKUDZ8fj+gI<8`#QMqI_qHxh z6<4=k!I9H*H3ThLI#Sq)Ic_&m>C#m9N=IJ)Ia@cSz4-p47ay>p+FEf`$C+K-v`@2A{EN3O`#B?`{be1;r!J78H^_OTCfWF4(zuC@~;6M;Pw^{Qmnr;c0?L~fv zZQsHVwN7`PPCQ{UihtQrMRV7uab|nFK_KB!G(DfkLwk`@g(8*-eewMD59*?6RDk7q z*+R3avx$$}%^8qeLE z^s065?d96HM=_K6SVGq=FetN2De(gu6oI3vz}97E|_;aD(gqbD#A!q!X_? zUJ3iPZHq#FQyPCyw{V)B3ebS=fdxjCQi?7DkVez;9>Sgb4)l#j7b&oCb?#9Z2yJaQRW>l7VUU&#J}{Rl+F|>}@nJJAw2VN_ZB} z@oGZu0w}tI4{myhI-_gSl)Dp=tj2-`U;0hjvGYo)i6b z#0>kw3wMNtSeGD_6e&wF_^NF*O-Gd6z>g8ls-*1n1VYcH!andJtzu1ib=R?&RsUE%TwLr>D6K0;v-CMn+}^Ngem9%T8+UX(u?SeH@+TPxtF}}TkVUcUXPeg)-o2*;JI~6MGe|xNH^+Kf>ps$VuV6hiI6uMfZb>QvB?>TNm5#xj{$^ZwFzzkp4Mi+!oCQ?(!z>8~W&?_Wm1?eYVUKV~D1WCCRpMh`dpSSN_6V0QwOe z_Tuq2P#2(oAdHQ}iny|>&}%U}5H^ZXMFW61g%fJz4WwzEky(*`blZdXH=Ud@H2Hs( zIz`NRylKOuqTSn7%O)#7fr-C9L4w?8VE7CLpcfAw|I&}GE~JiQW#+YqW&prH83Wp1 zYMH<45k$N$h^t$0Vz4wlIASgU$2U#2XjcSz2;{OvDG9r}A)KeTJ)8&V6ko_lLV%D_ zyH@f}e_+C`S0;+09C(ABtRF~+6N3%_VE$5f}<5+Y|U9XAAdgVlSLpz5z`&(=j=(^2? zxNp1fe3fw)*2y~9ikxKkhr6vyLyE!ev;KA10G&!M6BFplmpcsZ`Cy;o&D|UY@FSy5 zb~s2AN?BEfVfztSOjbh!QA$9?`>K|E`2iu=^1v?5&>-%a^NX4=Lx#Ebt8HBUq(YUOtmvG)d?JbU(2DnIX}+&> z+ALDUGA(ws<5qv&J}DC1pSg$#41V)*q58u}KJBxKg6yZObElw5UT-e#ZEYOD&R6im zaxLO+F}_2j49`;NQF@fdpCb3}!R*%dVN@A;_10*hsY0p>P!Z2=RZ2)2j~)Ln;p^YJZ~y(o^iJjw%UR__{#%>C=VTET&0dW zrDx!>(5dleyu7-pB&3$J?Y<<~py4oZEYXwQ?7#p#ixlu2W77qKUSBt}<)n(GXiDL% za20xspXZ+SE}zE3Sx)EpLfNm+>Pw5VQGUyJ{3>Z?3@OhpLf?{v&;-m0*d%&waK6~; z>JW<=$lr*Mywn)&NZ|GxyY#-si*1SAJ`-$O$9et<5bd0<)TJA)FnYaM?~o{QVNkws zM0Ci3RhTguOnLI=-$|D!`d$MM)DLw+^t|?ADF$vQ3|@CZpL&SG`6e4zSvP+r@)lm` zapxP~B0wRZR^P|uldI9SVs6+TEn-No=_eG()r0PYx?cSlX(zleMOpt&T_a!t6+Aj_ z!=w*PF<8p>3?q5n>8lFt<(?8x|Fh^E(gSg?DF zK6m}WASEJ$%PsQJY~F-smRPB(gjMI0KyBUOQKujac1YCMn`-3sTnuM+tu4~@;I~9k zL`;y7JQQ0qj@l8tPV4f54};Hw>|IM1pz^rNfVB2`=X+>t|F@Z-0=sr_k`Nxa^wVkk z5Tk!pASiu_$M8+<^IV{B4h6%&N3jA$IUFf8oBIH-^89kE=(4YSOfSw8vnv&z-7YK= ztywj?4UUk?bS%L*$Ngc`$j{wa8}S@tqr7>)I8(~s*HGcdWcEI(@}%dc-~Y`Uhk2+? zI-*XB&W?3j&HnCmNbhv#uG*;Bc>1I%Sg@Z?PhSQ{>JD>{BxIIW0e_G=;f?^s#rR?q|%=5.2.4" + }, + "suggest": { + "paragonie/random_compat": "Provides better randomness in PHP 5.x" + }, + "require-dev": { + "mikey179/vfsStream": "1.1.*" + } +} \ No newline at end of file diff --git a/contributing.md b/contributing.md new file mode 100644 index 0000000..2037e04 --- /dev/null +++ b/contributing.md @@ -0,0 +1,95 @@ +# Contributing to CodeIgniter + + +CodeIgniter is a community driven project and accepts contributions of code and documentation from the community. These contributions are made in the form of Issues or [Pull Requests](http://help.github.com/send-pull-requests/) on the [CodeIgniter repository](https://github.com/bcit-ci/CodeIgniter>) on GitHub. + +Issues are a quick way to point out a bug. If you find a bug or documentation error in CodeIgniter then please check a few things first: + +1. There is not already an open Issue +2. The issue has already been fixed (check the develop branch, or look for closed Issues) +3. Is it something really obvious that you can fix yourself? + +Reporting issues is helpful but an even better approach is to send a Pull Request, which is done by "Forking" the main repository and committing to your own copy. This will require you to use the version control system called Git. + +## Guidelines + +Before we look into how, here are the guidelines. If your Pull Requests fail +to pass these guidelines it will be declined and you will need to re-submit +when you’ve made the changes. This might sound a bit tough, but it is required +for us to maintain quality of the code-base. + +### PHP Style + +All code must meet the [Style Guide](https://codeigniter.com/user_guide/general/styleguide.html), which is +essentially the [Allman indent style](https://en.wikipedia.org/wiki/Indent_style#Allman_style), underscores and readable operators. This makes certain that all code is the same format as the existing code and means it will be as readable as possible. + +### Documentation + +If you change anything that requires a change to documentation then you will need to add it. New classes, methods, parameters, changing default values, etc are all things that will require a change to documentation. The change-log must also be updated for every change. Also PHPDoc blocks must be maintained. + +### Compatibility + +CodeIgniter recommends PHP 5.4 or newer to be used, but it should be +compatible with PHP 5.2.4 so all code supplied must stick to this +requirement. If PHP 5.3 (and above) functions or features are used then +there must be a fallback for PHP 5.2.4. + +### Branching + +CodeIgniter uses the [Git-Flow](http://nvie.com/posts/a-successful-git-branching-model/) branching model which requires all pull requests to be sent to the "develop" branch. This is +where the next planned version will be developed. The "master" branch will always contain the latest stable version and is kept clean so a "hotfix" (e.g: an emergency security patch) can be applied to master to create a new version, without worrying about other features holding it up. For this reason all commits need to be made to "develop" and any sent to "master" will be closed automatically. If you have multiple changes to submit, please place all changes into their own branch on your fork. + +One thing at a time: A pull request should only contain one change. That does not mean only one commit, but one change - however many commits it took. The reason for this is that if you change X and Y but send a pull request for both at the same time, we might really want X but disagree with Y, meaning we cannot merge the request. Using the Git-Flow branching model you can create new branches for both of these features and send two requests. + +### Signing + +You must sign your work, certifying that you either wrote the work or otherwise have the right to pass it on to an open source project. git makes this trivial as you merely have to use `--signoff` on your commits to your CodeIgniter fork. + +`git commit --signoff` + +or simply + +`git commit -s` + +This will sign your commits with the information setup in your git config, e.g. + +`Signed-off-by: John Q Public ` + +If you are using [Tower](http://www.git-tower.com/) there is a "Sign-Off" checkbox in the commit window. You could even alias git commit to use the `-s` flag so you don’t have to think about it. + +By signing your work in this manner, you certify to a "Developer's Certificate of Origin". The current version of this certificate is in the `DCO.txt` file in the root of this repository. + + +## How-to Guide + +There are two ways to make changes, the easy way and the hard way. Either way you will need to [create a GitHub account](https://github.com/signup/free). + +Easy way GitHub allows in-line editing of files for making simple typo changes and quick-fixes. This is not the best way as you are unable to test the code works. If you do this you could be introducing syntax errors, etc, but for a Git-phobic user this is good for a quick-fix. + +Hard way The best way to contribute is to "clone" your fork of CodeIgniter to your development area. That sounds like some jargon, but "forking" on GitHub means "making a copy of that repo to your account" and "cloning" means "copying that code to your environment so you can work on it". + +1. Set up Git (Windows, Mac & Linux) +2. Go to the CodeIgniter repo +3. Fork it +4. Clone your CodeIgniter repo: git@github.com:/CodeIgniter.git +5. Checkout the "develop" branch At this point you are ready to start making changes. +6. Fix existing bugs on the Issue tracker after taking a look to see nobody else is working on them. +7. Commit the files +8. Push your develop branch to your fork +9. Send a pull request [http://help.github.com/send-pull-requests/](http://help.github.com/send-pull-requests/) + +The Reactor Engineers will now be alerted about the change and at least one of the team will respond. If your change fails to meet the guidelines it will be bounced, or feedback will be provided to help you improve it. + +Once the Reactor Engineer handling your pull request is happy with it they will merge it into develop and your patch will be part of the next release. + +### Keeping your fork up-to-date + +Unlike systems like Subversion, Git can have multiple remotes. A remote is the name for a URL of a Git repository. By default your fork will have a remote named "origin" which points to your fork, but you can add another remote named "codeigniter" which points to `git://github.com/bcit-ci/CodeIgniter.git`. This is a read-only remote but you can pull from this develop branch to update your own. + +If you are using command-line you can do the following: + +1. `git remote add codeigniter git://github.com/bcit-ci/CodeIgniter.git` +2. `git pull codeigniter develop` +3. `git push origin develop` + +Now your fork is up to date. This should be done regularly, or before you send a pull request at least. \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..d02b6bb --- /dev/null +++ b/index.php @@ -0,0 +1,315 @@ +=')) + { + error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED); + } + else + { + error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_USER_NOTICE); + } + break; + + default: + header('HTTP/1.1 503 Service Unavailable.', TRUE, 503); + echo 'The application environment is not set correctly.'; + exit(1); // EXIT_ERROR +} + +/* + *--------------------------------------------------------------- + * SYSTEM DIRECTORY NAME + *--------------------------------------------------------------- + * + * This variable must contain the name of your "system" directory. + * Set the path if it is not in the same directory as this file. + */ + $system_path = 'system'; + +/* + *--------------------------------------------------------------- + * APPLICATION DIRECTORY NAME + *--------------------------------------------------------------- + * + * If you want this front controller to use a different "application" + * directory than the default one you can set its name here. The directory + * can also be renamed or relocated anywhere on your server. If you do, + * use an absolute (full) server path. + * For more info please see the user guide: + * + * https://codeigniter.com/user_guide/general/managing_apps.html + * + * NO TRAILING SLASH! + */ + $application_folder = 'application'; + +/* + *--------------------------------------------------------------- + * VIEW DIRECTORY NAME + *--------------------------------------------------------------- + * + * If you want to move the view directory out of the application + * directory, set the path to it here. The directory can be renamed + * and relocated anywhere on your server. If blank, it will default + * to the standard location inside your application directory. + * If you do move this, use an absolute (full) server path. + * + * NO TRAILING SLASH! + */ + $view_folder = ''; + + +/* + * -------------------------------------------------------------------- + * DEFAULT CONTROLLER + * -------------------------------------------------------------------- + * + * Normally you will set your default controller in the routes.php file. + * You can, however, force a custom routing by hard-coding a + * specific controller class/function here. For most applications, you + * WILL NOT set your routing here, but it's an option for those + * special instances where you might want to override the standard + * routing in a specific front controller that shares a common CI installation. + * + * IMPORTANT: If you set the routing here, NO OTHER controller will be + * callable. In essence, this preference limits your application to ONE + * specific controller. Leave the function name blank if you need + * to call functions dynamically via the URI. + * + * Un-comment the $routing array below to use this feature + */ + // The directory name, relative to the "controllers" directory. Leave blank + // if your controller is not in a sub-directory within the "controllers" one + // $routing['directory'] = ''; + + // The controller class file name. Example: mycontroller + // $routing['controller'] = ''; + + // The controller function you wish to be called. + // $routing['function'] = ''; + + +/* + * ------------------------------------------------------------------- + * CUSTOM CONFIG VALUES + * ------------------------------------------------------------------- + * + * The $assign_to_config array below will be passed dynamically to the + * config class when initialized. This allows you to set custom config + * items or override any default config values found in the config.php file. + * This can be handy as it permits you to share one application between + * multiple front controller files, with each file containing different + * config values. + * + * Un-comment the $assign_to_config array below to use this feature + */ + // $assign_to_config['name_of_config_item'] = 'value of config item'; + + + +// -------------------------------------------------------------------- +// END OF USER CONFIGURABLE SETTINGS. DO NOT EDIT BELOW THIS LINE +// -------------------------------------------------------------------- + +/* + * --------------------------------------------------------------- + * Resolve the system path for increased reliability + * --------------------------------------------------------------- + */ + + // Set the current directory correctly for CLI requests + if (defined('STDIN')) + { + chdir(dirname(__FILE__)); + } + + if (($_temp = realpath($system_path)) !== FALSE) + { + $system_path = $_temp.DIRECTORY_SEPARATOR; + } + else + { + // Ensure there's a trailing slash + $system_path = strtr( + rtrim($system_path, '/\\'), + '/\\', + DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR + ).DIRECTORY_SEPARATOR; + } + + // Is the system path correct? + if ( ! is_dir($system_path)) + { + header('HTTP/1.1 503 Service Unavailable.', TRUE, 503); + echo 'Your system folder path does not appear to be set correctly. Please open the following file and correct this: '.pathinfo(__FILE__, PATHINFO_BASENAME); + exit(3); // EXIT_CONFIG + } + +/* + * ------------------------------------------------------------------- + * Now that we know the path, set the main path constants + * ------------------------------------------------------------------- + */ + // The name of THIS file + define('SELF', pathinfo(__FILE__, PATHINFO_BASENAME)); + + // Path to the system directory + define('BASEPATH', $system_path); + + // Path to the front controller (this file) directory + define('FCPATH', dirname(__FILE__).DIRECTORY_SEPARATOR); + + // Name of the "system" directory + define('SYSDIR', basename(BASEPATH)); + + // The path to the "application" directory + if (is_dir($application_folder)) + { + if (($_temp = realpath($application_folder)) !== FALSE) + { + $application_folder = $_temp; + } + else + { + $application_folder = strtr( + rtrim($application_folder, '/\\'), + '/\\', + DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR + ); + } + } + elseif (is_dir(BASEPATH.$application_folder.DIRECTORY_SEPARATOR)) + { + $application_folder = BASEPATH.strtr( + trim($application_folder, '/\\'), + '/\\', + DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR + ); + } + else + { + header('HTTP/1.1 503 Service Unavailable.', TRUE, 503); + echo 'Your application folder path does not appear to be set correctly. Please open the following file and correct this: '.SELF; + exit(3); // EXIT_CONFIG + } + + define('APPPATH', $application_folder.DIRECTORY_SEPARATOR); + + // The path to the "views" directory + if ( ! isset($view_folder[0]) && is_dir(APPPATH.'views'.DIRECTORY_SEPARATOR)) + { + $view_folder = APPPATH.'views'; + } + elseif (is_dir($view_folder)) + { + if (($_temp = realpath($view_folder)) !== FALSE) + { + $view_folder = $_temp; + } + else + { + $view_folder = strtr( + rtrim($view_folder, '/\\'), + '/\\', + DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR + ); + } + } + elseif (is_dir(APPPATH.$view_folder.DIRECTORY_SEPARATOR)) + { + $view_folder = APPPATH.strtr( + trim($view_folder, '/\\'), + '/\\', + DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR + ); + } + else + { + header('HTTP/1.1 503 Service Unavailable.', TRUE, 503); + echo 'Your view folder path does not appear to be set correctly. Please open the following file and correct this: '.SELF; + exit(3); // EXIT_CONFIG + } + + define('VIEWPATH', $view_folder.DIRECTORY_SEPARATOR); + +/* + * -------------------------------------------------------------------- + * LOAD THE BOOTSTRAP FILE + * -------------------------------------------------------------------- + * + * And away we go... + */ +require_once BASEPATH.'core/CodeIgniter.php'; diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..388eee3 --- /dev/null +++ b/license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 - 2016, British Columbia Institute of Technology + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/readme.rst b/readme.rst new file mode 100644 index 0000000..237c7d9 --- /dev/null +++ b/readme.rst @@ -0,0 +1,57 @@ +################### +ciBlog +################### + +This is a simple blog application built on CodeIgniter 3.x. It is from the YouTube series [Build a CodeIgniter PHP App](https://www.youtube.com/watch?v=I752ofYu7ag) + +******************* +Usage +******************* + +Create the database with the posts table and upload to your host + +************************** +SQL +************************** + +CREATE TABLE `posts` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `category_id` int(11) NOT NULL, + `user_id` int(11) NOT NULL, + `title` varchar(255) NOT NULL, + `slug` varchar(255) NOT NULL, + `body` text NOT NULL, + `post_image` varchar(255) NOT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB; + + +CREATE TABLE `categories` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `name` varchar(255) NOT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB; + +CREATE TABLE `comments` ( + `id` int(11) NOT NULL, + `post_id` int(11) NOT NULL, + `name` varchar(255) NOT NULL, + `email` varchar(255) NOT NULL, + `body` text NOT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB; + +CREATE TABLE `users` ( + `id` int(11) NOT NULL, + `name` varchar(255) NOT NULL, + `zipcode` varchar(255) NOT NULL, + `email` varchar(255) NOT NULL, + `username` varchar(255) NOT NULL, + `password` varchar(255) NOT NULL, + `register_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB; diff --git a/system/.htaccess b/system/.htaccess new file mode 100644 index 0000000..97c65d2 --- /dev/null +++ b/system/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + \ No newline at end of file diff --git a/system/core/Benchmark.php b/system/core/Benchmark.php new file mode 100644 index 0000000..b1d74f7 --- /dev/null +++ b/system/core/Benchmark.php @@ -0,0 +1,133 @@ +marker[$name] = microtime(TRUE); + } + + // -------------------------------------------------------------------- + + /** + * Elapsed time + * + * Calculates the time difference between two marked points. + * + * If the first parameter is empty this function instead returns the + * {elapsed_time} pseudo-variable. This permits the full system + * execution time to be shown in a template. The output class will + * swap the real value for this variable. + * + * @param string $point1 A particular marked point + * @param string $point2 A particular marked point + * @param int $decimals Number of decimal places + * + * @return string Calculated elapsed time on success, + * an '{elapsed_string}' if $point1 is empty + * or an empty string if $point1 is not found. + */ + public function elapsed_time($point1 = '', $point2 = '', $decimals = 4) + { + if ($point1 === '') + { + return '{elapsed_time}'; + } + + if ( ! isset($this->marker[$point1])) + { + return ''; + } + + if ( ! isset($this->marker[$point2])) + { + $this->marker[$point2] = microtime(TRUE); + } + + return number_format($this->marker[$point2] - $this->marker[$point1], $decimals); + } + + // -------------------------------------------------------------------- + + /** + * Memory Usage + * + * Simply returns the {memory_usage} marker. + * + * This permits it to be put it anywhere in a template + * without the memory being calculated until the end. + * The output class will swap the real value for this variable. + * + * @return string '{memory_usage}' + */ + public function memory_usage() + { + return '{memory_usage}'; + } + +} diff --git a/system/core/CodeIgniter.php b/system/core/CodeIgniter.php new file mode 100644 index 0000000..a2067fb --- /dev/null +++ b/system/core/CodeIgniter.php @@ -0,0 +1,556 @@ + '_ENV', 'G' => '_GET', 'P' => '_POST', 'C' => '_COOKIE', 'S' => '_SERVER') as $key => $superglobal) + { + if (strpos($_registered, $key) === FALSE) + { + continue; + } + + foreach (array_keys($$superglobal) as $var) + { + if (isset($GLOBALS[$var]) && ! in_array($var, $_protected, TRUE)) + { + $GLOBALS[$var] = NULL; + } + } + } + } +} + + +/* + * ------------------------------------------------------ + * Define a custom error handler so we can log PHP errors + * ------------------------------------------------------ + */ + set_error_handler('_error_handler'); + set_exception_handler('_exception_handler'); + register_shutdown_function('_shutdown_handler'); + +/* + * ------------------------------------------------------ + * Set the subclass_prefix + * ------------------------------------------------------ + * + * Normally the "subclass_prefix" is set in the config file. + * The subclass prefix allows CI to know if a core class is + * being extended via a library in the local application + * "libraries" folder. Since CI allows config items to be + * overridden via data set in the main index.php file, + * before proceeding we need to know if a subclass_prefix + * override exists. If so, we will set this value now, + * before any classes are loaded + * Note: Since the config file data is cached it doesn't + * hurt to load it here. + */ + if ( ! empty($assign_to_config['subclass_prefix'])) + { + get_config(array('subclass_prefix' => $assign_to_config['subclass_prefix'])); + } + +/* + * ------------------------------------------------------ + * Should we use a Composer autoloader? + * ------------------------------------------------------ + */ + if ($composer_autoload = config_item('composer_autoload')) + { + if ($composer_autoload === TRUE) + { + file_exists(APPPATH.'vendor/autoload.php') + ? require_once(APPPATH.'vendor/autoload.php') + : log_message('error', '$config[\'composer_autoload\'] is set to TRUE but '.APPPATH.'vendor/autoload.php was not found.'); + } + elseif (file_exists($composer_autoload)) + { + require_once($composer_autoload); + } + else + { + log_message('error', 'Could not find the specified $config[\'composer_autoload\'] path: '.$composer_autoload); + } + } + +/* + * ------------------------------------------------------ + * Start the timer... tick tock tick tock... + * ------------------------------------------------------ + */ + $BM =& load_class('Benchmark', 'core'); + $BM->mark('total_execution_time_start'); + $BM->mark('loading_time:_base_classes_start'); + +/* + * ------------------------------------------------------ + * Instantiate the hooks class + * ------------------------------------------------------ + */ + $EXT =& load_class('Hooks', 'core'); + +/* + * ------------------------------------------------------ + * Is there a "pre_system" hook? + * ------------------------------------------------------ + */ + $EXT->call_hook('pre_system'); + +/* + * ------------------------------------------------------ + * Instantiate the config class + * ------------------------------------------------------ + * + * Note: It is important that Config is loaded first as + * most other classes depend on it either directly or by + * depending on another class that uses it. + * + */ + $CFG =& load_class('Config', 'core'); + + // Do we have any manually set config items in the index.php file? + if (isset($assign_to_config) && is_array($assign_to_config)) + { + foreach ($assign_to_config as $key => $value) + { + $CFG->set_item($key, $value); + } + } + +/* + * ------------------------------------------------------ + * Important charset-related stuff + * ------------------------------------------------------ + * + * Configure mbstring and/or iconv if they are enabled + * and set MB_ENABLED and ICONV_ENABLED constants, so + * that we don't repeatedly do extension_loaded() or + * function_exists() calls. + * + * Note: UTF-8 class depends on this. It used to be done + * in it's constructor, but it's _not_ class-specific. + * + */ + $charset = strtoupper(config_item('charset')); + ini_set('default_charset', $charset); + + if (extension_loaded('mbstring')) + { + define('MB_ENABLED', TRUE); + // mbstring.internal_encoding is deprecated starting with PHP 5.6 + // and it's usage triggers E_DEPRECATED messages. + @ini_set('mbstring.internal_encoding', $charset); + // This is required for mb_convert_encoding() to strip invalid characters. + // That's utilized by CI_Utf8, but it's also done for consistency with iconv. + mb_substitute_character('none'); + } + else + { + define('MB_ENABLED', FALSE); + } + + // There's an ICONV_IMPL constant, but the PHP manual says that using + // iconv's predefined constants is "strongly discouraged". + if (extension_loaded('iconv')) + { + define('ICONV_ENABLED', TRUE); + // iconv.internal_encoding is deprecated starting with PHP 5.6 + // and it's usage triggers E_DEPRECATED messages. + @ini_set('iconv.internal_encoding', $charset); + } + else + { + define('ICONV_ENABLED', FALSE); + } + + if (is_php('5.6')) + { + ini_set('php.internal_encoding', $charset); + } + +/* + * ------------------------------------------------------ + * Load compatibility features + * ------------------------------------------------------ + */ + + require_once(BASEPATH.'core/compat/mbstring.php'); + require_once(BASEPATH.'core/compat/hash.php'); + require_once(BASEPATH.'core/compat/password.php'); + require_once(BASEPATH.'core/compat/standard.php'); + +/* + * ------------------------------------------------------ + * Instantiate the UTF-8 class + * ------------------------------------------------------ + */ + $UNI =& load_class('Utf8', 'core'); + +/* + * ------------------------------------------------------ + * Instantiate the URI class + * ------------------------------------------------------ + */ + $URI =& load_class('URI', 'core'); + +/* + * ------------------------------------------------------ + * Instantiate the routing class and set the routing + * ------------------------------------------------------ + */ + $RTR =& load_class('Router', 'core', isset($routing) ? $routing : NULL); + +/* + * ------------------------------------------------------ + * Instantiate the output class + * ------------------------------------------------------ + */ + $OUT =& load_class('Output', 'core'); + +/* + * ------------------------------------------------------ + * Is there a valid cache file? If so, we're done... + * ------------------------------------------------------ + */ + if ($EXT->call_hook('cache_override') === FALSE && $OUT->_display_cache($CFG, $URI) === TRUE) + { + exit; + } + +/* + * ----------------------------------------------------- + * Load the security class for xss and csrf support + * ----------------------------------------------------- + */ + $SEC =& load_class('Security', 'core'); + +/* + * ------------------------------------------------------ + * Load the Input class and sanitize globals + * ------------------------------------------------------ + */ + $IN =& load_class('Input', 'core'); + +/* + * ------------------------------------------------------ + * Load the Language class + * ------------------------------------------------------ + */ + $LANG =& load_class('Lang', 'core'); + +/* + * ------------------------------------------------------ + * Load the app controller and local controller + * ------------------------------------------------------ + * + */ + // Load the base controller class + require_once BASEPATH.'core/Controller.php'; + + /** + * Reference to the CI_Controller method. + * + * Returns current CI instance object + * + * @return CI_Controller + */ + function &get_instance() + { + return CI_Controller::get_instance(); + } + + if (file_exists(APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php')) + { + require_once APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php'; + } + + // Set a mark point for benchmarking + $BM->mark('loading_time:_base_classes_end'); + +/* + * ------------------------------------------------------ + * Sanity checks + * ------------------------------------------------------ + * + * The Router class has already validated the request, + * leaving us with 3 options here: + * + * 1) an empty class name, if we reached the default + * controller, but it didn't exist; + * 2) a query string which doesn't go through a + * file_exists() check + * 3) a regular request for a non-existing page + * + * We handle all of these as a 404 error. + * + * Furthermore, none of the methods in the app controller + * or the loader class can be called via the URI, nor can + * controller methods that begin with an underscore. + */ + + $e404 = FALSE; + $class = ucfirst($RTR->class); + $method = $RTR->method; + + if (empty($class) OR ! file_exists(APPPATH.'controllers/'.$RTR->directory.$class.'.php')) + { + $e404 = TRUE; + } + else + { + require_once(APPPATH.'controllers/'.$RTR->directory.$class.'.php'); + + if ( ! class_exists($class, FALSE) OR $method[0] === '_' OR method_exists('CI_Controller', $method)) + { + $e404 = TRUE; + } + elseif (method_exists($class, '_remap')) + { + $params = array($method, array_slice($URI->rsegments, 2)); + $method = '_remap'; + } + elseif ( ! method_exists($class, $method)) + { + $e404 = TRUE; + } + /** + * DO NOT CHANGE THIS, NOTHING ELSE WORKS! + * + * - method_exists() returns true for non-public methods, which passes the previous elseif + * - is_callable() returns false for PHP 4-style constructors, even if there's a __construct() + * - method_exists($class, '__construct') won't work because CI_Controller::__construct() is inherited + * - People will only complain if this doesn't work, even though it is documented that it shouldn't. + * + * ReflectionMethod::isConstructor() is the ONLY reliable check, + * knowing which method will be executed as a constructor. + */ + elseif ( ! is_callable(array($class, $method)) && strcasecmp($class, $method) === 0) + { + $reflection = new ReflectionMethod($class, $method); + if ( ! $reflection->isPublic() OR $reflection->isConstructor()) + { + $e404 = TRUE; + } + } + } + + if ($e404) + { + if ( ! empty($RTR->routes['404_override'])) + { + if (sscanf($RTR->routes['404_override'], '%[^/]/%s', $error_class, $error_method) !== 2) + { + $error_method = 'index'; + } + + $error_class = ucfirst($error_class); + + if ( ! class_exists($error_class, FALSE)) + { + if (file_exists(APPPATH.'controllers/'.$RTR->directory.$error_class.'.php')) + { + require_once(APPPATH.'controllers/'.$RTR->directory.$error_class.'.php'); + $e404 = ! class_exists($error_class, FALSE); + } + // Were we in a directory? If so, check for a global override + elseif ( ! empty($RTR->directory) && file_exists(APPPATH.'controllers/'.$error_class.'.php')) + { + require_once(APPPATH.'controllers/'.$error_class.'.php'); + if (($e404 = ! class_exists($error_class, FALSE)) === FALSE) + { + $RTR->directory = ''; + } + } + } + else + { + $e404 = FALSE; + } + } + + // Did we reset the $e404 flag? If so, set the rsegments, starting from index 1 + if ( ! $e404) + { + $class = $error_class; + $method = $error_method; + + $URI->rsegments = array( + 1 => $class, + 2 => $method + ); + } + else + { + show_404($RTR->directory.$class.'/'.$method); + } + } + + if ($method !== '_remap') + { + $params = array_slice($URI->rsegments, 2); + } + +/* + * ------------------------------------------------------ + * Is there a "pre_controller" hook? + * ------------------------------------------------------ + */ + $EXT->call_hook('pre_controller'); + +/* + * ------------------------------------------------------ + * Instantiate the requested controller + * ------------------------------------------------------ + */ + // Mark a start point so we can benchmark the controller + $BM->mark('controller_execution_time_( '.$class.' / '.$method.' )_start'); + + $CI = new $class(); + +/* + * ------------------------------------------------------ + * Is there a "post_controller_constructor" hook? + * ------------------------------------------------------ + */ + $EXT->call_hook('post_controller_constructor'); + +/* + * ------------------------------------------------------ + * Call the requested method + * ------------------------------------------------------ + */ + call_user_func_array(array(&$CI, $method), $params); + + // Mark a benchmark end point + $BM->mark('controller_execution_time_( '.$class.' / '.$method.' )_end'); + +/* + * ------------------------------------------------------ + * Is there a "post_controller" hook? + * ------------------------------------------------------ + */ + $EXT->call_hook('post_controller'); + +/* + * ------------------------------------------------------ + * Send the final rendered output to the browser + * ------------------------------------------------------ + */ + if ($EXT->call_hook('display_override') === FALSE) + { + $OUT->_display(); + } + +/* + * ------------------------------------------------------ + * Is there a "post_system" hook? + * ------------------------------------------------------ + */ + $EXT->call_hook('post_system'); diff --git a/system/core/Common.php b/system/core/Common.php new file mode 100644 index 0000000..91c585f --- /dev/null +++ b/system/core/Common.php @@ -0,0 +1,857 @@ +='); + } + + return $_is_php[$version]; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('is_really_writable')) +{ + /** + * Tests for file writability + * + * is_writable() returns TRUE on Windows servers when you really can't write to + * the file, based on the read-only attribute. is_writable() is also unreliable + * on Unix servers if safe_mode is on. + * + * @link https://bugs.php.net/bug.php?id=54709 + * @param string + * @return bool + */ + function is_really_writable($file) + { + // If we're on a Unix server with safe_mode off we call is_writable + if (DIRECTORY_SEPARATOR === '/' && (is_php('5.4') OR ! ini_get('safe_mode'))) + { + return is_writable($file); + } + + /* For Windows servers and safe_mode "on" installations we'll actually + * write a file then read it. Bah... + */ + if (is_dir($file)) + { + $file = rtrim($file, '/').'/'.md5(mt_rand()); + if (($fp = @fopen($file, 'ab')) === FALSE) + { + return FALSE; + } + + fclose($fp); + @chmod($file, 0777); + @unlink($file); + return TRUE; + } + elseif ( ! is_file($file) OR ($fp = @fopen($file, 'ab')) === FALSE) + { + return FALSE; + } + + fclose($fp); + return TRUE; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('load_class')) +{ + /** + * Class registry + * + * This function acts as a singleton. If the requested class does not + * exist it is instantiated and set to a static variable. If it has + * previously been instantiated the variable is returned. + * + * @param string the class name being requested + * @param string the directory where the class should be found + * @param string an optional argument to pass to the class constructor + * @return object + */ + function &load_class($class, $directory = 'libraries', $param = NULL) + { + static $_classes = array(); + + // Does the class exist? If so, we're done... + if (isset($_classes[$class])) + { + return $_classes[$class]; + } + + $name = FALSE; + + // Look for the class first in the local application/libraries folder + // then in the native system/libraries folder + foreach (array(APPPATH, BASEPATH) as $path) + { + if (file_exists($path.$directory.'/'.$class.'.php')) + { + $name = 'CI_'.$class; + + if (class_exists($name, FALSE) === FALSE) + { + require_once($path.$directory.'/'.$class.'.php'); + } + + break; + } + } + + // Is the request a class extension? If so we load it too + if (file_exists(APPPATH.$directory.'/'.config_item('subclass_prefix').$class.'.php')) + { + $name = config_item('subclass_prefix').$class; + + if (class_exists($name, FALSE) === FALSE) + { + require_once(APPPATH.$directory.'/'.$name.'.php'); + } + } + + // Did we find the class? + if ($name === FALSE) + { + // Note: We use exit() rather than show_error() in order to avoid a + // self-referencing loop with the Exceptions class + set_status_header(503); + echo 'Unable to locate the specified class: '.$class.'.php'; + exit(5); // EXIT_UNK_CLASS + } + + // Keep track of what we just loaded + is_loaded($class); + + $_classes[$class] = isset($param) + ? new $name($param) + : new $name(); + return $_classes[$class]; + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('is_loaded')) +{ + /** + * Keeps track of which libraries have been loaded. This function is + * called by the load_class() function above + * + * @param string + * @return array + */ + function &is_loaded($class = '') + { + static $_is_loaded = array(); + + if ($class !== '') + { + $_is_loaded[strtolower($class)] = $class; + } + + return $_is_loaded; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('get_config')) +{ + /** + * Loads the main config.php file + * + * This function lets us grab the config file even if the Config class + * hasn't been instantiated yet + * + * @param array + * @return array + */ + function &get_config(Array $replace = array()) + { + static $config; + + if (empty($config)) + { + $file_path = APPPATH.'config/config.php'; + $found = FALSE; + if (file_exists($file_path)) + { + $found = TRUE; + require($file_path); + } + + // Is the config file in the environment folder? + if (file_exists($file_path = APPPATH.'config/'.ENVIRONMENT.'/config.php')) + { + require($file_path); + } + elseif ( ! $found) + { + set_status_header(503); + echo 'The configuration file does not exist.'; + exit(3); // EXIT_CONFIG + } + + // Does the $config array exist in the file? + if ( ! isset($config) OR ! is_array($config)) + { + set_status_header(503); + echo 'Your config file does not appear to be formatted correctly.'; + exit(3); // EXIT_CONFIG + } + } + + // Are any values being dynamically added or replaced? + foreach ($replace as $key => $val) + { + $config[$key] = $val; + } + + return $config; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('config_item')) +{ + /** + * Returns the specified config item + * + * @param string + * @return mixed + */ + function config_item($item) + { + static $_config; + + if (empty($_config)) + { + // references cannot be directly assigned to static variables, so we use an array + $_config[0] =& get_config(); + } + + return isset($_config[0][$item]) ? $_config[0][$item] : NULL; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('get_mimes')) +{ + /** + * Returns the MIME types array from config/mimes.php + * + * @return array + */ + function &get_mimes() + { + static $_mimes; + + if (empty($_mimes)) + { + if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/mimes.php')) + { + $_mimes = include(APPPATH.'config/'.ENVIRONMENT.'/mimes.php'); + } + elseif (file_exists(APPPATH.'config/mimes.php')) + { + $_mimes = include(APPPATH.'config/mimes.php'); + } + else + { + $_mimes = array(); + } + } + + return $_mimes; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('is_https')) +{ + /** + * Is HTTPS? + * + * Determines if the application is accessed via an encrypted + * (HTTPS) connection. + * + * @return bool + */ + function is_https() + { + if ( ! empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off') + { + return TRUE; + } + elseif (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') + { + return TRUE; + } + elseif ( ! empty($_SERVER['HTTP_FRONT_END_HTTPS']) && strtolower($_SERVER['HTTP_FRONT_END_HTTPS']) !== 'off') + { + return TRUE; + } + + return FALSE; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('is_cli')) +{ + + /** + * Is CLI? + * + * Test to see if a request was made from the command line. + * + * @return bool + */ + function is_cli() + { + return (PHP_SAPI === 'cli' OR defined('STDIN')); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('show_error')) +{ + /** + * Error Handler + * + * This function lets us invoke the exception class and + * display errors using the standard error template located + * in application/views/errors/error_general.php + * This function will send the error page directly to the + * browser and exit. + * + * @param string + * @param int + * @param string + * @return void + */ + function show_error($message, $status_code = 500, $heading = 'An Error Was Encountered') + { + $status_code = abs($status_code); + if ($status_code < 100) + { + $exit_status = $status_code + 9; // 9 is EXIT__AUTO_MIN + if ($exit_status > 125) // 125 is EXIT__AUTO_MAX + { + $exit_status = 1; // EXIT_ERROR + } + + $status_code = 500; + } + else + { + $exit_status = 1; // EXIT_ERROR + } + + $_error =& load_class('Exceptions', 'core'); + echo $_error->show_error($heading, $message, 'error_general', $status_code); + exit($exit_status); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('show_404')) +{ + /** + * 404 Page Handler + * + * This function is similar to the show_error() function above + * However, instead of the standard error template it displays + * 404 errors. + * + * @param string + * @param bool + * @return void + */ + function show_404($page = '', $log_error = TRUE) + { + $_error =& load_class('Exceptions', 'core'); + $_error->show_404($page, $log_error); + exit(4); // EXIT_UNKNOWN_FILE + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('log_message')) +{ + /** + * Error Logging Interface + * + * We use this as a simple mechanism to access the logging + * class and send messages to be logged. + * + * @param string the error level: 'error', 'debug' or 'info' + * @param string the error message + * @return void + */ + function log_message($level, $message) + { + static $_log; + + if ($_log === NULL) + { + // references cannot be directly assigned to static variables, so we use an array + $_log[0] =& load_class('Log', 'core'); + } + + $_log[0]->write_log($level, $message); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('set_status_header')) +{ + /** + * Set HTTP Status Header + * + * @param int the status code + * @param string + * @return void + */ + function set_status_header($code = 200, $text = '') + { + if (is_cli()) + { + return; + } + + if (empty($code) OR ! is_numeric($code)) + { + show_error('Status codes must be numeric', 500); + } + + if (empty($text)) + { + is_int($code) OR $code = (int) $code; + $stati = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + + 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', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 511 => 'Network Authentication Required', + ); + + if (isset($stati[$code])) + { + $text = $stati[$code]; + } + else + { + show_error('No status text available. Please check your status code number or supply your own message text.', 500); + } + } + + if (strpos(PHP_SAPI, 'cgi') === 0) + { + header('Status: '.$code.' '.$text, TRUE); + } + else + { + $server_protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1'; + header($server_protocol.' '.$code.' '.$text, TRUE, $code); + } + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('_error_handler')) +{ + /** + * Error Handler + * + * This is the custom error handler that is declared at the (relative) + * top of CodeIgniter.php. The main reason we use this is to permit + * PHP errors to be logged in our own log files since the user may + * not have access to server logs. Since this function effectively + * intercepts PHP errors, however, we also need to display errors + * based on the current error_reporting level. + * We do that with the use of a PHP error template. + * + * @param int $severity + * @param string $message + * @param string $filepath + * @param int $line + * @return void + */ + function _error_handler($severity, $message, $filepath, $line) + { + $is_error = (((E_ERROR | E_PARSE | E_COMPILE_ERROR | E_CORE_ERROR | E_USER_ERROR) & $severity) === $severity); + + // When an error occurred, set the status header to '500 Internal Server Error' + // to indicate to the client something went wrong. + // This can't be done within the $_error->show_php_error method because + // it is only called when the display_errors flag is set (which isn't usually + // the case in a production environment) or when errors are ignored because + // they are above the error_reporting threshold. + if ($is_error) + { + set_status_header(500); + } + + // Should we ignore the error? We'll get the current error_reporting + // level and add its bits with the severity bits to find out. + if (($severity & error_reporting()) !== $severity) + { + return; + } + + $_error =& load_class('Exceptions', 'core'); + $_error->log_exception($severity, $message, $filepath, $line); + + // Should we display the error? + if (str_ireplace(array('off', 'none', 'no', 'false', 'null'), '', ini_get('display_errors'))) + { + $_error->show_php_error($severity, $message, $filepath, $line); + } + + // If the error is fatal, the execution of the script should be stopped because + // errors can't be recovered from. Halting the script conforms with PHP's + // default error handling. See http://www.php.net/manual/en/errorfunc.constants.php + if ($is_error) + { + exit(1); // EXIT_ERROR + } + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('_exception_handler')) +{ + /** + * Exception Handler + * + * Sends uncaught exceptions to the logger and displays them + * only if display_errors is On so that they don't show up in + * production environments. + * + * @param Exception $exception + * @return void + */ + function _exception_handler($exception) + { + $_error =& load_class('Exceptions', 'core'); + $_error->log_exception('error', 'Exception: '.$exception->getMessage(), $exception->getFile(), $exception->getLine()); + + is_cli() OR set_status_header(500); + // Should we display the error? + if (str_ireplace(array('off', 'none', 'no', 'false', 'null'), '', ini_get('display_errors'))) + { + $_error->show_exception($exception); + } + + exit(1); // EXIT_ERROR + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('_shutdown_handler')) +{ + /** + * Shutdown Handler + * + * This is the shutdown handler that is declared at the top + * of CodeIgniter.php. The main reason we use this is to simulate + * a complete custom exception handler. + * + * E_STRICT is purposively neglected because such events may have + * been caught. Duplication or none? None is preferred for now. + * + * @link http://insomanic.me.uk/post/229851073/php-trick-catching-fatal-errors-e-error-with-a + * @return void + */ + function _shutdown_handler() + { + $last_error = error_get_last(); + if (isset($last_error) && + ($last_error['type'] & (E_ERROR | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING))) + { + _error_handler($last_error['type'], $last_error['message'], $last_error['file'], $last_error['line']); + } + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('remove_invisible_characters')) +{ + /** + * Remove Invisible Characters + * + * This prevents sandwiching null characters + * between ascii characters, like Java\0script. + * + * @param string + * @param bool + * @return string + */ + function remove_invisible_characters($str, $url_encoded = TRUE) + { + $non_displayables = array(); + + // every control character except newline (dec 10), + // carriage return (dec 13) and horizontal tab (dec 09) + if ($url_encoded) + { + $non_displayables[] = '/%0[0-8bcef]/i'; // url encoded 00-08, 11, 12, 14, 15 + $non_displayables[] = '/%1[0-9a-f]/i'; // url encoded 16-31 + } + + $non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127 + + do + { + $str = preg_replace($non_displayables, '', $str, -1, $count); + } + while ($count); + + return $str; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('html_escape')) +{ + /** + * Returns HTML escaped variable. + * + * @param mixed $var The input string or array of strings to be escaped. + * @param bool $double_encode $double_encode set to FALSE prevents escaping twice. + * @return mixed The escaped string or array of strings as a result. + */ + function html_escape($var, $double_encode = TRUE) + { + if (empty($var)) + { + return $var; + } + + if (is_array($var)) + { + foreach (array_keys($var) as $key) + { + $var[$key] = html_escape($var[$key], $double_encode); + } + + return $var; + } + + return htmlspecialchars($var, ENT_QUOTES, config_item('charset'), $double_encode); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('_stringify_attributes')) +{ + /** + * Stringify attributes for use in HTML tags. + * + * Helper function used to convert a string, array, or object + * of attributes to a string. + * + * @param mixed string, array, object + * @param bool + * @return string + */ + function _stringify_attributes($attributes, $js = FALSE) + { + $atts = NULL; + + if (empty($attributes)) + { + return $atts; + } + + if (is_string($attributes)) + { + return ' '.$attributes; + } + + $attributes = (array) $attributes; + + foreach ($attributes as $key => $val) + { + $atts .= ($js) ? $key.'='.$val.',' : ' '.$key.'="'.$val.'"'; + } + + return rtrim($atts, ','); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('function_usable')) +{ + /** + * Function usable + * + * Executes a function_exists() check, and if the Suhosin PHP + * extension is loaded - checks whether the function that is + * checked might be disabled in there as well. + * + * This is useful as function_exists() will return FALSE for + * functions disabled via the *disable_functions* php.ini + * setting, but not for *suhosin.executor.func.blacklist* and + * *suhosin.executor.disable_eval*. These settings will just + * terminate script execution if a disabled function is executed. + * + * The above described behavior turned out to be a bug in Suhosin, + * but even though a fix was commited for 0.9.34 on 2012-02-12, + * that version is yet to be released. This function will therefore + * be just temporary, but would probably be kept for a few years. + * + * @link http://www.hardened-php.net/suhosin/ + * @param string $function_name Function to check for + * @return bool TRUE if the function exists and is safe to call, + * FALSE otherwise. + */ + function function_usable($function_name) + { + static $_suhosin_func_blacklist; + + if (function_exists($function_name)) + { + if ( ! isset($_suhosin_func_blacklist)) + { + $_suhosin_func_blacklist = extension_loaded('suhosin') + ? explode(',', trim(ini_get('suhosin.executor.func.blacklist'))) + : array(); + } + + return ! in_array($function_name, $_suhosin_func_blacklist, TRUE); + } + + return FALSE; + } +} diff --git a/system/core/Config.php b/system/core/Config.php new file mode 100644 index 0000000..9fd3e4a --- /dev/null +++ b/system/core/Config.php @@ -0,0 +1,379 @@ +config =& get_config(); + + // Set the base_url automatically if none was provided + if (empty($this->config['base_url'])) + { + if (isset($_SERVER['SERVER_ADDR'])) + { + if (strpos($_SERVER['SERVER_ADDR'], ':') !== FALSE) + { + $server_addr = '['.$_SERVER['SERVER_ADDR'].']'; + } + else + { + $server_addr = $_SERVER['SERVER_ADDR']; + } + + $base_url = (is_https() ? 'https' : 'http').'://'.$server_addr + .substr($_SERVER['SCRIPT_NAME'], 0, strpos($_SERVER['SCRIPT_NAME'], basename($_SERVER['SCRIPT_FILENAME']))); + } + else + { + $base_url = 'http://localhost/'; + } + + $this->set_item('base_url', $base_url); + } + + log_message('info', 'Config Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Load Config File + * + * @param string $file Configuration file name + * @param bool $use_sections Whether configuration values should be loaded into their own section + * @param bool $fail_gracefully Whether to just return FALSE or display an error message + * @return bool TRUE if the file was loaded correctly or FALSE on failure + */ + public function load($file = '', $use_sections = FALSE, $fail_gracefully = FALSE) + { + $file = ($file === '') ? 'config' : str_replace('.php', '', $file); + $loaded = FALSE; + + foreach ($this->_config_paths as $path) + { + foreach (array($file, ENVIRONMENT.DIRECTORY_SEPARATOR.$file) as $location) + { + $file_path = $path.'config/'.$location.'.php'; + if (in_array($file_path, $this->is_loaded, TRUE)) + { + return TRUE; + } + + if ( ! file_exists($file_path)) + { + continue; + } + + include($file_path); + + if ( ! isset($config) OR ! is_array($config)) + { + if ($fail_gracefully === TRUE) + { + return FALSE; + } + + show_error('Your '.$file_path.' file does not appear to contain a valid configuration array.'); + } + + if ($use_sections === TRUE) + { + $this->config[$file] = isset($this->config[$file]) + ? array_merge($this->config[$file], $config) + : $config; + } + else + { + $this->config = array_merge($this->config, $config); + } + + $this->is_loaded[] = $file_path; + $config = NULL; + $loaded = TRUE; + log_message('debug', 'Config file loaded: '.$file_path); + } + } + + if ($loaded === TRUE) + { + return TRUE; + } + elseif ($fail_gracefully === TRUE) + { + return FALSE; + } + + show_error('The configuration file '.$file.'.php does not exist.'); + } + + // -------------------------------------------------------------------- + + /** + * Fetch a config file item + * + * @param string $item Config item name + * @param string $index Index name + * @return string|null The configuration item or NULL if the item doesn't exist + */ + public function item($item, $index = '') + { + if ($index == '') + { + return isset($this->config[$item]) ? $this->config[$item] : NULL; + } + + return isset($this->config[$index], $this->config[$index][$item]) ? $this->config[$index][$item] : NULL; + } + + // -------------------------------------------------------------------- + + /** + * Fetch a config file item with slash appended (if not empty) + * + * @param string $item Config item name + * @return string|null The configuration item or NULL if the item doesn't exist + */ + public function slash_item($item) + { + if ( ! isset($this->config[$item])) + { + return NULL; + } + elseif (trim($this->config[$item]) === '') + { + return ''; + } + + return rtrim($this->config[$item], '/').'/'; + } + + // -------------------------------------------------------------------- + + /** + * Site URL + * + * Returns base_url . index_page [. uri_string] + * + * @uses CI_Config::_uri_string() + * + * @param string|string[] $uri URI string or an array of segments + * @param string $protocol + * @return string + */ + public function site_url($uri = '', $protocol = NULL) + { + $base_url = $this->slash_item('base_url'); + + if (isset($protocol)) + { + // For protocol-relative links + if ($protocol === '') + { + $base_url = substr($base_url, strpos($base_url, '//')); + } + else + { + $base_url = $protocol.substr($base_url, strpos($base_url, '://')); + } + } + + if (empty($uri)) + { + return $base_url.$this->item('index_page'); + } + + $uri = $this->_uri_string($uri); + + if ($this->item('enable_query_strings') === FALSE) + { + $suffix = isset($this->config['url_suffix']) ? $this->config['url_suffix'] : ''; + + if ($suffix !== '') + { + if (($offset = strpos($uri, '?')) !== FALSE) + { + $uri = substr($uri, 0, $offset).$suffix.substr($uri, $offset); + } + else + { + $uri .= $suffix; + } + } + + return $base_url.$this->slash_item('index_page').$uri; + } + elseif (strpos($uri, '?') === FALSE) + { + $uri = '?'.$uri; + } + + return $base_url.$this->item('index_page').$uri; + } + + // ------------------------------------------------------------- + + /** + * Base URL + * + * Returns base_url [. uri_string] + * + * @uses CI_Config::_uri_string() + * + * @param string|string[] $uri URI string or an array of segments + * @param string $protocol + * @return string + */ + public function base_url($uri = '', $protocol = NULL) + { + $base_url = $this->slash_item('base_url'); + + if (isset($protocol)) + { + // For protocol-relative links + if ($protocol === '') + { + $base_url = substr($base_url, strpos($base_url, '//')); + } + else + { + $base_url = $protocol.substr($base_url, strpos($base_url, '://')); + } + } + + return $base_url.$this->_uri_string($uri); + } + + // ------------------------------------------------------------- + + /** + * Build URI string + * + * @used-by CI_Config::site_url() + * @used-by CI_Config::base_url() + * + * @param string|string[] $uri URI string or an array of segments + * @return string + */ + protected function _uri_string($uri) + { + if ($this->item('enable_query_strings') === FALSE) + { + is_array($uri) && $uri = implode('/', $uri); + return ltrim($uri, '/'); + } + elseif (is_array($uri)) + { + return http_build_query($uri); + } + + return $uri; + } + + // -------------------------------------------------------------------- + + /** + * System URL + * + * @deprecated 3.0.0 Encourages insecure practices + * @return string + */ + public function system_url() + { + $x = explode('/', preg_replace('|/*(.+?)/*$|', '\\1', BASEPATH)); + return $this->slash_item('base_url').end($x).'/'; + } + + // -------------------------------------------------------------------- + + /** + * Set a config file item + * + * @param string $item Config item key + * @param string $value Config item value + * @return void + */ + public function set_item($item, $value) + { + $this->config[$item] = $value; + } + +} diff --git a/system/core/Controller.php b/system/core/Controller.php new file mode 100644 index 0000000..83b3df2 --- /dev/null +++ b/system/core/Controller.php @@ -0,0 +1,96 @@ + $class) + { + $this->$var =& load_class($class); + } + + $this->load =& load_class('Loader', 'core'); + $this->load->initialize(); + log_message('info', 'Controller Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Get the CI singleton + * + * @static + * @return object + */ + public static function &get_instance() + { + return self::$instance; + } + +} diff --git a/system/core/Exceptions.php b/system/core/Exceptions.php new file mode 100644 index 0000000..4e10f28 --- /dev/null +++ b/system/core/Exceptions.php @@ -0,0 +1,274 @@ + 'Error', + E_WARNING => 'Warning', + E_PARSE => 'Parsing Error', + E_NOTICE => 'Notice', + E_CORE_ERROR => 'Core Error', + E_CORE_WARNING => 'Core Warning', + E_COMPILE_ERROR => 'Compile Error', + E_COMPILE_WARNING => 'Compile Warning', + E_USER_ERROR => 'User Error', + E_USER_WARNING => 'User Warning', + E_USER_NOTICE => 'User Notice', + E_STRICT => 'Runtime Notice' + ); + + /** + * Class constructor + * + * @return void + */ + public function __construct() + { + $this->ob_level = ob_get_level(); + // Note: Do not log messages from this constructor. + } + + // -------------------------------------------------------------------- + + /** + * Exception Logger + * + * Logs PHP generated error messages + * + * @param int $severity Log level + * @param string $message Error message + * @param string $filepath File path + * @param int $line Line number + * @return void + */ + public function log_exception($severity, $message, $filepath, $line) + { + $severity = isset($this->levels[$severity]) ? $this->levels[$severity] : $severity; + log_message('error', 'Severity: '.$severity.' --> '.$message.' '.$filepath.' '.$line); + } + + // -------------------------------------------------------------------- + + /** + * 404 Error Handler + * + * @uses CI_Exceptions::show_error() + * + * @param string $page Page URI + * @param bool $log_error Whether to log the error + * @return void + */ + public function show_404($page = '', $log_error = TRUE) + { + if (is_cli()) + { + $heading = 'Not Found'; + $message = 'The controller/method pair you requested was not found.'; + } + else + { + $heading = '404 Page Not Found'; + $message = 'The page you requested was not found.'; + } + + // By default we log this, but allow a dev to skip it + if ($log_error) + { + log_message('error', $heading.': '.$page); + } + + echo $this->show_error($heading, $message, 'error_404', 404); + exit(4); // EXIT_UNKNOWN_FILE + } + + // -------------------------------------------------------------------- + + /** + * General Error Page + * + * Takes an error message as input (either as a string or an array) + * and displays it using the specified template. + * + * @param string $heading Page heading + * @param string|string[] $message Error message + * @param string $template Template name + * @param int $status_code (default: 500) + * + * @return string Error page output + */ + public function show_error($heading, $message, $template = 'error_general', $status_code = 500) + { + $templates_path = config_item('error_views_path'); + if (empty($templates_path)) + { + $templates_path = VIEWPATH.'errors'.DIRECTORY_SEPARATOR; + } + + if (is_cli()) + { + $message = "\t".(is_array($message) ? implode("\n\t", $message) : $message); + $template = 'cli'.DIRECTORY_SEPARATOR.$template; + } + else + { + set_status_header($status_code); + $message = '

'.(is_array($message) ? implode('

', $message) : $message).'

'; + $template = 'html'.DIRECTORY_SEPARATOR.$template; + } + + if (ob_get_level() > $this->ob_level + 1) + { + ob_end_flush(); + } + ob_start(); + include($templates_path.$template.'.php'); + $buffer = ob_get_contents(); + ob_end_clean(); + return $buffer; + } + + // -------------------------------------------------------------------- + + public function show_exception($exception) + { + $templates_path = config_item('error_views_path'); + if (empty($templates_path)) + { + $templates_path = VIEWPATH.'errors'.DIRECTORY_SEPARATOR; + } + + $message = $exception->getMessage(); + if (empty($message)) + { + $message = '(null)'; + } + + if (is_cli()) + { + $templates_path .= 'cli'.DIRECTORY_SEPARATOR; + } + else + { + $templates_path .= 'html'.DIRECTORY_SEPARATOR; + } + + if (ob_get_level() > $this->ob_level + 1) + { + ob_end_flush(); + } + + ob_start(); + include($templates_path.'error_exception.php'); + $buffer = ob_get_contents(); + ob_end_clean(); + echo $buffer; + } + + // -------------------------------------------------------------------- + + /** + * Native PHP error handler + * + * @param int $severity Error level + * @param string $message Error message + * @param string $filepath File path + * @param int $line Line number + * @return string Error page output + */ + public function show_php_error($severity, $message, $filepath, $line) + { + $templates_path = config_item('error_views_path'); + if (empty($templates_path)) + { + $templates_path = VIEWPATH.'errors'.DIRECTORY_SEPARATOR; + } + + $severity = isset($this->levels[$severity]) ? $this->levels[$severity] : $severity; + + // For safety reasons we don't show the full file path in non-CLI requests + if ( ! is_cli()) + { + $filepath = str_replace('\\', '/', $filepath); + if (FALSE !== strpos($filepath, '/')) + { + $x = explode('/', $filepath); + $filepath = $x[count($x)-2].'/'.end($x); + } + + $template = 'html'.DIRECTORY_SEPARATOR.'error_php'; + } + else + { + $template = 'cli'.DIRECTORY_SEPARATOR.'error_php'; + } + + if (ob_get_level() > $this->ob_level + 1) + { + ob_end_flush(); + } + ob_start(); + include($templates_path.$template.'.php'); + $buffer = ob_get_contents(); + ob_end_clean(); + echo $buffer; + } + +} diff --git a/system/core/Hooks.php b/system/core/Hooks.php new file mode 100644 index 0000000..856795c --- /dev/null +++ b/system/core/Hooks.php @@ -0,0 +1,266 @@ +item('enable_hooks') === FALSE) + { + return; + } + + // Grab the "hooks" definition file. + if (file_exists(APPPATH.'config/hooks.php')) + { + include(APPPATH.'config/hooks.php'); + } + + if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/hooks.php')) + { + include(APPPATH.'config/'.ENVIRONMENT.'/hooks.php'); + } + + // If there are no hooks, we're done. + if ( ! isset($hook) OR ! is_array($hook)) + { + return; + } + + $this->hooks =& $hook; + $this->enabled = TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Call Hook + * + * Calls a particular hook. Called by CodeIgniter.php. + * + * @uses CI_Hooks::_run_hook() + * + * @param string $which Hook name + * @return bool TRUE on success or FALSE on failure + */ + public function call_hook($which = '') + { + if ( ! $this->enabled OR ! isset($this->hooks[$which])) + { + return FALSE; + } + + if (is_array($this->hooks[$which]) && ! isset($this->hooks[$which]['function'])) + { + foreach ($this->hooks[$which] as $val) + { + $this->_run_hook($val); + } + } + else + { + $this->_run_hook($this->hooks[$which]); + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Run Hook + * + * Runs a particular hook + * + * @param array $data Hook details + * @return bool TRUE on success or FALSE on failure + */ + protected function _run_hook($data) + { + // Closures/lambda functions and array($object, 'method') callables + if (is_callable($data)) + { + is_array($data) + ? $data[0]->{$data[1]}() + : $data(); + + return TRUE; + } + elseif ( ! is_array($data)) + { + return FALSE; + } + + // ----------------------------------- + // Safety - Prevents run-away loops + // ----------------------------------- + + // If the script being called happens to have the same + // hook call within it a loop can happen + if ($this->_in_progress === TRUE) + { + return; + } + + // ----------------------------------- + // Set file path + // ----------------------------------- + + if ( ! isset($data['filepath'], $data['filename'])) + { + return FALSE; + } + + $filepath = APPPATH.$data['filepath'].'/'.$data['filename']; + + if ( ! file_exists($filepath)) + { + return FALSE; + } + + // Determine and class and/or function names + $class = empty($data['class']) ? FALSE : $data['class']; + $function = empty($data['function']) ? FALSE : $data['function']; + $params = isset($data['params']) ? $data['params'] : ''; + + if (empty($function)) + { + return FALSE; + } + + // Set the _in_progress flag + $this->_in_progress = TRUE; + + // Call the requested class and/or function + if ($class !== FALSE) + { + // The object is stored? + if (isset($this->_objects[$class])) + { + if (method_exists($this->_objects[$class], $function)) + { + $this->_objects[$class]->$function($params); + } + else + { + return $this->_in_progress = FALSE; + } + } + else + { + class_exists($class, FALSE) OR require_once($filepath); + + if ( ! class_exists($class, FALSE) OR ! method_exists($class, $function)) + { + return $this->_in_progress = FALSE; + } + + // Store the object and execute the method + $this->_objects[$class] = new $class(); + $this->_objects[$class]->$function($params); + } + } + else + { + function_exists($function) OR require_once($filepath); + + if ( ! function_exists($function)) + { + return $this->_in_progress = FALSE; + } + + $function($params); + } + + $this->_in_progress = FALSE; + return TRUE; + } + +} diff --git a/system/core/Input.php b/system/core/Input.php new file mode 100644 index 0000000..b81d51e --- /dev/null +++ b/system/core/Input.php @@ -0,0 +1,897 @@ +_allow_get_array = (config_item('allow_get_array') === TRUE); + $this->_enable_xss = (config_item('global_xss_filtering') === TRUE); + $this->_enable_csrf = (config_item('csrf_protection') === TRUE); + $this->_standardize_newlines = (bool) config_item('standardize_newlines'); + + $this->security =& load_class('Security', 'core'); + + // Do we need the UTF-8 class? + if (UTF8_ENABLED === TRUE) + { + $this->uni =& load_class('Utf8', 'core'); + } + + // Sanitize global arrays + $this->_sanitize_globals(); + + // CSRF Protection check + if ($this->_enable_csrf === TRUE && ! is_cli()) + { + $this->security->csrf_verify(); + } + + log_message('info', 'Input Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Fetch from array + * + * Internal method used to retrieve values from global arrays. + * + * @param array &$array $_GET, $_POST, $_COOKIE, $_SERVER, etc. + * @param mixed $index Index for item to be fetched from $array + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + protected function _fetch_from_array(&$array, $index = NULL, $xss_clean = NULL) + { + is_bool($xss_clean) OR $xss_clean = $this->_enable_xss; + + // If $index is NULL, it means that the whole $array is requested + isset($index) OR $index = array_keys($array); + + // allow fetching multiple keys at once + if (is_array($index)) + { + $output = array(); + foreach ($index as $key) + { + $output[$key] = $this->_fetch_from_array($array, $key, $xss_clean); + } + + return $output; + } + + if (isset($array[$index])) + { + $value = $array[$index]; + } + elseif (($count = preg_match_all('/(?:^[^\[]+)|\[[^]]*\]/', $index, $matches)) > 1) // Does the index contain array notation + { + $value = $array; + for ($i = 0; $i < $count; $i++) + { + $key = trim($matches[0][$i], '[]'); + if ($key === '') // Empty notation will return the value as array + { + break; + } + + if (isset($value[$key])) + { + $value = $value[$key]; + } + else + { + return NULL; + } + } + } + else + { + return NULL; + } + + return ($xss_clean === TRUE) + ? $this->security->xss_clean($value) + : $value; + } + + // -------------------------------------------------------------------- + + /** + * Fetch an item from the GET array + * + * @param mixed $index Index for item to be fetched from $_GET + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + public function get($index = NULL, $xss_clean = NULL) + { + return $this->_fetch_from_array($_GET, $index, $xss_clean); + } + + // -------------------------------------------------------------------- + + /** + * Fetch an item from the POST array + * + * @param mixed $index Index for item to be fetched from $_POST + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + public function post($index = NULL, $xss_clean = NULL) + { + return $this->_fetch_from_array($_POST, $index, $xss_clean); + } + + // -------------------------------------------------------------------- + + /** + * Fetch an item from POST data with fallback to GET + * + * @param string $index Index for item to be fetched from $_POST or $_GET + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + public function post_get($index, $xss_clean = NULL) + { + return isset($_POST[$index]) + ? $this->post($index, $xss_clean) + : $this->get($index, $xss_clean); + } + + // -------------------------------------------------------------------- + + /** + * Fetch an item from GET data with fallback to POST + * + * @param string $index Index for item to be fetched from $_GET or $_POST + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + public function get_post($index, $xss_clean = NULL) + { + return isset($_GET[$index]) + ? $this->get($index, $xss_clean) + : $this->post($index, $xss_clean); + } + + // -------------------------------------------------------------------- + + /** + * Fetch an item from the COOKIE array + * + * @param mixed $index Index for item to be fetched from $_COOKIE + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + public function cookie($index = NULL, $xss_clean = NULL) + { + return $this->_fetch_from_array($_COOKIE, $index, $xss_clean); + } + + // -------------------------------------------------------------------- + + /** + * Fetch an item from the SERVER array + * + * @param mixed $index Index for item to be fetched from $_SERVER + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + public function server($index, $xss_clean = NULL) + { + return $this->_fetch_from_array($_SERVER, $index, $xss_clean); + } + + // ------------------------------------------------------------------------ + + /** + * Fetch an item from the php://input stream + * + * Useful when you need to access PUT, DELETE or PATCH request data. + * + * @param string $index Index for item to be fetched + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + public function input_stream($index = NULL, $xss_clean = NULL) + { + // Prior to PHP 5.6, the input stream can only be read once, + // so we'll need to check if we have already done that first. + if ( ! is_array($this->_input_stream)) + { + // $this->raw_input_stream will trigger __get(). + parse_str($this->raw_input_stream, $this->_input_stream); + is_array($this->_input_stream) OR $this->_input_stream = array(); + } + + return $this->_fetch_from_array($this->_input_stream, $index, $xss_clean); + } + + // ------------------------------------------------------------------------ + + /** + * Set cookie + * + * Accepts an arbitrary number of parameters (up to 7) or an associative + * array in the first parameter containing all the values. + * + * @param string|mixed[] $name Cookie name or an array containing parameters + * @param string $value Cookie value + * @param int $expire Cookie expiration time in seconds + * @param string $domain Cookie domain (e.g.: '.yourdomain.com') + * @param string $path Cookie path (default: '/') + * @param string $prefix Cookie name prefix + * @param bool $secure Whether to only transfer cookies via SSL + * @param bool $httponly Whether to only makes the cookie accessible via HTTP (no javascript) + * @return void + */ + public function set_cookie($name, $value = '', $expire = '', $domain = '', $path = '/', $prefix = '', $secure = FALSE, $httponly = FALSE) + { + if (is_array($name)) + { + // always leave 'name' in last place, as the loop will break otherwise, due to $$item + foreach (array('value', 'expire', 'domain', 'path', 'prefix', 'secure', 'httponly', 'name') as $item) + { + if (isset($name[$item])) + { + $$item = $name[$item]; + } + } + } + + if ($prefix === '' && config_item('cookie_prefix') !== '') + { + $prefix = config_item('cookie_prefix'); + } + + if ($domain == '' && config_item('cookie_domain') != '') + { + $domain = config_item('cookie_domain'); + } + + if ($path === '/' && config_item('cookie_path') !== '/') + { + $path = config_item('cookie_path'); + } + + if ($secure === FALSE && config_item('cookie_secure') === TRUE) + { + $secure = config_item('cookie_secure'); + } + + if ($httponly === FALSE && config_item('cookie_httponly') !== FALSE) + { + $httponly = config_item('cookie_httponly'); + } + + if ( ! is_numeric($expire)) + { + $expire = time() - 86500; + } + else + { + $expire = ($expire > 0) ? time() + $expire : 0; + } + + setcookie($prefix.$name, $value, $expire, $path, $domain, $secure, $httponly); + } + + // -------------------------------------------------------------------- + + /** + * Fetch the IP Address + * + * Determines and validates the visitor's IP address. + * + * @return string IP address + */ + public function ip_address() + { + if ($this->ip_address !== FALSE) + { + return $this->ip_address; + } + + $proxy_ips = config_item('proxy_ips'); + if ( ! empty($proxy_ips) && ! is_array($proxy_ips)) + { + $proxy_ips = explode(',', str_replace(' ', '', $proxy_ips)); + } + + $this->ip_address = $this->server('REMOTE_ADDR'); + + if ($proxy_ips) + { + foreach (array('HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'HTTP_X_CLIENT_IP', 'HTTP_X_CLUSTER_CLIENT_IP') as $header) + { + if (($spoof = $this->server($header)) !== NULL) + { + // Some proxies typically list the whole chain of IP + // addresses through which the client has reached us. + // e.g. client_ip, proxy_ip1, proxy_ip2, etc. + sscanf($spoof, '%[^,]', $spoof); + + if ( ! $this->valid_ip($spoof)) + { + $spoof = NULL; + } + else + { + break; + } + } + } + + if ($spoof) + { + for ($i = 0, $c = count($proxy_ips); $i < $c; $i++) + { + // Check if we have an IP address or a subnet + if (strpos($proxy_ips[$i], '/') === FALSE) + { + // An IP address (and not a subnet) is specified. + // We can compare right away. + if ($proxy_ips[$i] === $this->ip_address) + { + $this->ip_address = $spoof; + break; + } + + continue; + } + + // We have a subnet ... now the heavy lifting begins + isset($separator) OR $separator = $this->valid_ip($this->ip_address, 'ipv6') ? ':' : '.'; + + // If the proxy entry doesn't match the IP protocol - skip it + if (strpos($proxy_ips[$i], $separator) === FALSE) + { + continue; + } + + // Convert the REMOTE_ADDR IP address to binary, if needed + if ( ! isset($ip, $sprintf)) + { + if ($separator === ':') + { + // Make sure we're have the "full" IPv6 format + $ip = explode(':', + str_replace('::', + str_repeat(':', 9 - substr_count($this->ip_address, ':')), + $this->ip_address + ) + ); + + for ($j = 0; $j < 8; $j++) + { + $ip[$j] = intval($ip[$j], 16); + } + + $sprintf = '%016b%016b%016b%016b%016b%016b%016b%016b'; + } + else + { + $ip = explode('.', $this->ip_address); + $sprintf = '%08b%08b%08b%08b'; + } + + $ip = vsprintf($sprintf, $ip); + } + + // Split the netmask length off the network address + sscanf($proxy_ips[$i], '%[^/]/%d', $netaddr, $masklen); + + // Again, an IPv6 address is most likely in a compressed form + if ($separator === ':') + { + $netaddr = explode(':', str_replace('::', str_repeat(':', 9 - substr_count($netaddr, ':')), $netaddr)); + for ($j = 0; $j < 8; $j++) + { + $netaddr[$i] = intval($netaddr[$j], 16); + } + } + else + { + $netaddr = explode('.', $netaddr); + } + + // Convert to binary and finally compare + if (strncmp($ip, vsprintf($sprintf, $netaddr), $masklen) === 0) + { + $this->ip_address = $spoof; + break; + } + } + } + } + + if ( ! $this->valid_ip($this->ip_address)) + { + return $this->ip_address = '0.0.0.0'; + } + + return $this->ip_address; + } + + // -------------------------------------------------------------------- + + /** + * Validate IP Address + * + * @param string $ip IP address + * @param string $which IP protocol: 'ipv4' or 'ipv6' + * @return bool + */ + public function valid_ip($ip, $which = '') + { + switch (strtolower($which)) + { + case 'ipv4': + $which = FILTER_FLAG_IPV4; + break; + case 'ipv6': + $which = FILTER_FLAG_IPV6; + break; + default: + $which = NULL; + break; + } + + return (bool) filter_var($ip, FILTER_VALIDATE_IP, $which); + } + + // -------------------------------------------------------------------- + + /** + * Fetch User Agent string + * + * @return string|null User Agent string or NULL if it doesn't exist + */ + public function user_agent($xss_clean = NULL) + { + return $this->_fetch_from_array($_SERVER, 'HTTP_USER_AGENT', $xss_clean); + } + + // -------------------------------------------------------------------- + + /** + * Sanitize Globals + * + * Internal method serving for the following purposes: + * + * - Unsets $_GET data, if query strings are not enabled + * - Cleans POST, COOKIE and SERVER data + * - Standardizes newline characters to PHP_EOL + * + * @return void + */ + protected function _sanitize_globals() + { + // Is $_GET data allowed? If not we'll set the $_GET to an empty array + if ($this->_allow_get_array === FALSE) + { + $_GET = array(); + } + elseif (is_array($_GET)) + { + foreach ($_GET as $key => $val) + { + $_GET[$this->_clean_input_keys($key)] = $this->_clean_input_data($val); + } + } + + // Clean $_POST Data + if (is_array($_POST)) + { + foreach ($_POST as $key => $val) + { + $_POST[$this->_clean_input_keys($key)] = $this->_clean_input_data($val); + } + } + + // Clean $_COOKIE Data + if (is_array($_COOKIE)) + { + // Also get rid of specially treated cookies that might be set by a server + // or silly application, that are of no use to a CI application anyway + // but that when present will trip our 'Disallowed Key Characters' alarm + // http://www.ietf.org/rfc/rfc2109.txt + // note that the key names below are single quoted strings, and are not PHP variables + unset( + $_COOKIE['$Version'], + $_COOKIE['$Path'], + $_COOKIE['$Domain'] + ); + + foreach ($_COOKIE as $key => $val) + { + if (($cookie_key = $this->_clean_input_keys($key)) !== FALSE) + { + $_COOKIE[$cookie_key] = $this->_clean_input_data($val); + } + else + { + unset($_COOKIE[$key]); + } + } + } + + // Sanitize PHP_SELF + $_SERVER['PHP_SELF'] = strip_tags($_SERVER['PHP_SELF']); + + log_message('debug', 'Global POST, GET and COOKIE data sanitized'); + } + + // -------------------------------------------------------------------- + + /** + * Clean Input Data + * + * Internal method that aids in escaping data and + * standardizing newline characters to PHP_EOL. + * + * @param string|string[] $str Input string(s) + * @return string + */ + protected function _clean_input_data($str) + { + if (is_array($str)) + { + $new_array = array(); + foreach (array_keys($str) as $key) + { + $new_array[$this->_clean_input_keys($key)] = $this->_clean_input_data($str[$key]); + } + return $new_array; + } + + /* We strip slashes if magic quotes is on to keep things consistent + + NOTE: In PHP 5.4 get_magic_quotes_gpc() will always return 0 and + it will probably not exist in future versions at all. + */ + if ( ! is_php('5.4') && get_magic_quotes_gpc()) + { + $str = stripslashes($str); + } + + // Clean UTF-8 if supported + if (UTF8_ENABLED === TRUE) + { + $str = $this->uni->clean_string($str); + } + + // Remove control characters + $str = remove_invisible_characters($str, FALSE); + + // Standardize newlines if needed + if ($this->_standardize_newlines === TRUE) + { + return preg_replace('/(?:\r\n|[\r\n])/', PHP_EOL, $str); + } + + return $str; + } + + // -------------------------------------------------------------------- + + /** + * Clean Keys + * + * Internal method that helps to prevent malicious users + * from trying to exploit keys we make sure that keys are + * only named with alpha-numeric text and a few other items. + * + * @param string $str Input string + * @param bool $fatal Whether to terminate script exection + * or to return FALSE if an invalid + * key is encountered + * @return string|bool + */ + protected function _clean_input_keys($str, $fatal = TRUE) + { + if ( ! preg_match('/^[a-z0-9:_\/|-]+$/i', $str)) + { + if ($fatal === TRUE) + { + return FALSE; + } + else + { + set_status_header(503); + echo 'Disallowed Key Characters.'; + exit(7); // EXIT_USER_INPUT + } + } + + // Clean UTF-8 if supported + if (UTF8_ENABLED === TRUE) + { + return $this->uni->clean_string($str); + } + + return $str; + } + + // -------------------------------------------------------------------- + + /** + * Request Headers + * + * @param bool $xss_clean Whether to apply XSS filtering + * @return array + */ + public function request_headers($xss_clean = FALSE) + { + // If header is already defined, return it immediately + if ( ! empty($this->headers)) + { + return $this->_fetch_from_array($this->headers, NULL, $xss_clean); + } + + // In Apache, you can simply call apache_request_headers() + if (function_exists('apache_request_headers')) + { + $this->headers = apache_request_headers(); + } + else + { + isset($_SERVER['CONTENT_TYPE']) && $this->headers['Content-Type'] = $_SERVER['CONTENT_TYPE']; + + foreach ($_SERVER as $key => $val) + { + if (sscanf($key, 'HTTP_%s', $header) === 1) + { + // take SOME_HEADER and turn it into Some-Header + $header = str_replace('_', ' ', strtolower($header)); + $header = str_replace(' ', '-', ucwords($header)); + + $this->headers[$header] = $_SERVER[$key]; + } + } + } + + return $this->_fetch_from_array($this->headers, NULL, $xss_clean); + } + + // -------------------------------------------------------------------- + + /** + * Get Request Header + * + * Returns the value of a single member of the headers class member + * + * @param string $index Header name + * @param bool $xss_clean Whether to apply XSS filtering + * @return string|null The requested header on success or NULL on failure + */ + public function get_request_header($index, $xss_clean = FALSE) + { + static $headers; + + if ( ! isset($headers)) + { + empty($this->headers) && $this->request_headers(); + foreach ($this->headers as $key => $value) + { + $headers[strtolower($key)] = $value; + } + } + + $index = strtolower($index); + + if ( ! isset($headers[$index])) + { + return NULL; + } + + return ($xss_clean === TRUE) + ? $this->security->xss_clean($headers[$index]) + : $headers[$index]; + } + + // -------------------------------------------------------------------- + + /** + * Is AJAX request? + * + * Test to see if a request contains the HTTP_X_REQUESTED_WITH header. + * + * @return bool + */ + public function is_ajax_request() + { + return ( ! empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'); + } + + // -------------------------------------------------------------------- + + /** + * Is CLI request? + * + * Test to see if a request was made from the command line. + * + * @deprecated 3.0.0 Use is_cli() instead + * @return bool + */ + public function is_cli_request() + { + return is_cli(); + } + + // -------------------------------------------------------------------- + + /** + * Get Request Method + * + * Return the request method + * + * @param bool $upper Whether to return in upper or lower case + * (default: FALSE) + * @return string + */ + public function method($upper = FALSE) + { + return ($upper) + ? strtoupper($this->server('REQUEST_METHOD')) + : strtolower($this->server('REQUEST_METHOD')); + } + + // ------------------------------------------------------------------------ + + /** + * Magic __get() + * + * Allows read access to protected properties + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + if ($name === 'raw_input_stream') + { + isset($this->_raw_input_stream) OR $this->_raw_input_stream = file_get_contents('php://input'); + return $this->_raw_input_stream; + } + elseif ($name === 'ip_address') + { + return $this->ip_address; + } + } + +} diff --git a/system/core/Lang.php b/system/core/Lang.php new file mode 100644 index 0000000..1fcff07 --- /dev/null +++ b/system/core/Lang.php @@ -0,0 +1,203 @@ +load($value, $idiom, $return, $add_suffix, $alt_path); + } + + return; + } + + $langfile = str_replace('.php', '', $langfile); + + if ($add_suffix === TRUE) + { + $langfile = preg_replace('/_lang$/', '', $langfile).'_lang'; + } + + $langfile .= '.php'; + + if (empty($idiom) OR ! preg_match('/^[a-z_-]+$/i', $idiom)) + { + $config =& get_config(); + $idiom = empty($config['language']) ? 'english' : $config['language']; + } + + if ($return === FALSE && isset($this->is_loaded[$langfile]) && $this->is_loaded[$langfile] === $idiom) + { + return; + } + + // Load the base file, so any others found can override it + $basepath = BASEPATH.'language/'.$idiom.'/'.$langfile; + if (($found = file_exists($basepath)) === TRUE) + { + include($basepath); + } + + // Do we have an alternative path to look in? + if ($alt_path !== '') + { + $alt_path .= 'language/'.$idiom.'/'.$langfile; + if (file_exists($alt_path)) + { + include($alt_path); + $found = TRUE; + } + } + else + { + foreach (get_instance()->load->get_package_paths(TRUE) as $package_path) + { + $package_path .= 'language/'.$idiom.'/'.$langfile; + if ($basepath !== $package_path && file_exists($package_path)) + { + include($package_path); + $found = TRUE; + break; + } + } + } + + if ($found !== TRUE) + { + show_error('Unable to load the requested language file: language/'.$idiom.'/'.$langfile); + } + + if ( ! isset($lang) OR ! is_array($lang)) + { + log_message('error', 'Language file contains no data: language/'.$idiom.'/'.$langfile); + + if ($return === TRUE) + { + return array(); + } + return; + } + + if ($return === TRUE) + { + return $lang; + } + + $this->is_loaded[$langfile] = $idiom; + $this->language = array_merge($this->language, $lang); + + log_message('info', 'Language file loaded: language/'.$idiom.'/'.$langfile); + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Language line + * + * Fetches a single line of text from the language array + * + * @param string $line Language line key + * @param bool $log_errors Whether to log an error message if the line is not found + * @return string Translation + */ + public function line($line, $log_errors = TRUE) + { + $value = isset($this->language[$line]) ? $this->language[$line] : FALSE; + + // Because killer robots like unicorns! + if ($value === FALSE && $log_errors === TRUE) + { + log_message('error', 'Could not find the language line "'.$line.'"'); + } + + return $value; + } + +} diff --git a/system/core/Loader.php b/system/core/Loader.php new file mode 100644 index 0000000..d2c3508 --- /dev/null +++ b/system/core/Loader.php @@ -0,0 +1,1437 @@ + TRUE); + + /** + * List of paths to load libraries from + * + * @var array + */ + protected $_ci_library_paths = array(APPPATH, BASEPATH); + + /** + * List of paths to load models from + * + * @var array + */ + protected $_ci_model_paths = array(APPPATH); + + /** + * List of paths to load helpers from + * + * @var array + */ + protected $_ci_helper_paths = array(APPPATH, BASEPATH); + + /** + * List of cached variables + * + * @var array + */ + protected $_ci_cached_vars = array(); + + /** + * List of loaded classes + * + * @var array + */ + protected $_ci_classes = array(); + + /** + * List of loaded models + * + * @var array + */ + protected $_ci_models = array(); + + /** + * List of loaded helpers + * + * @var array + */ + protected $_ci_helpers = array(); + + /** + * List of class name mappings + * + * @var array + */ + protected $_ci_varmap = array( + 'unit_test' => 'unit', + 'user_agent' => 'agent' + ); + + // -------------------------------------------------------------------- + + /** + * Class constructor + * + * Sets component load paths, gets the initial output buffering level. + * + * @return void + */ + public function __construct() + { + $this->_ci_ob_level = ob_get_level(); + $this->_ci_classes =& is_loaded(); + + log_message('info', 'Loader Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Initializer + * + * @todo Figure out a way to move this to the constructor + * without breaking *package_path*() methods. + * @uses CI_Loader::_ci_autoloader() + * @used-by CI_Controller::__construct() + * @return void + */ + public function initialize() + { + $this->_ci_autoloader(); + } + + // -------------------------------------------------------------------- + + /** + * Is Loaded + * + * A utility method to test if a class is in the self::$_ci_classes array. + * + * @used-by Mainly used by Form Helper function _get_validation_object(). + * + * @param string $class Class name to check for + * @return string|bool Class object name if loaded or FALSE + */ + public function is_loaded($class) + { + return array_search(ucfirst($class), $this->_ci_classes, TRUE); + } + + // -------------------------------------------------------------------- + + /** + * Library Loader + * + * Loads and instantiates libraries. + * Designed to be called from application controllers. + * + * @param string $library Library name + * @param array $params Optional parameters to pass to the library class constructor + * @param string $object_name An optional object name to assign to + * @return object + */ + public function library($library, $params = NULL, $object_name = NULL) + { + if (empty($library)) + { + return $this; + } + elseif (is_array($library)) + { + foreach ($library as $key => $value) + { + if (is_int($key)) + { + $this->library($value, $params); + } + else + { + $this->library($key, $params, $value); + } + } + + return $this; + } + + if ($params !== NULL && ! is_array($params)) + { + $params = NULL; + } + + $this->_ci_load_library($library, $params, $object_name); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Model Loader + * + * Loads and instantiates models. + * + * @param string $model Model name + * @param string $name An optional object name to assign to + * @param bool $db_conn An optional database connection configuration to initialize + * @return object + */ + public function model($model, $name = '', $db_conn = FALSE) + { + if (empty($model)) + { + return $this; + } + elseif (is_array($model)) + { + foreach ($model as $key => $value) + { + is_int($key) ? $this->model($value, '', $db_conn) : $this->model($key, $value, $db_conn); + } + + return $this; + } + + $path = ''; + + // Is the model in a sub-folder? If so, parse out the filename and path. + if (($last_slash = strrpos($model, '/')) !== FALSE) + { + // The path is in front of the last slash + $path = substr($model, 0, ++$last_slash); + + // And the model name behind it + $model = substr($model, $last_slash); + } + + if (empty($name)) + { + $name = $model; + } + + if (in_array($name, $this->_ci_models, TRUE)) + { + return $this; + } + + $CI =& get_instance(); + if (isset($CI->$name)) + { + throw new RuntimeException('The model name you are loading is the name of a resource that is already being used: '.$name); + } + + if ($db_conn !== FALSE && ! class_exists('CI_DB', FALSE)) + { + if ($db_conn === TRUE) + { + $db_conn = ''; + } + + $this->database($db_conn, FALSE, TRUE); + } + + // Note: All of the code under this condition used to be just: + // + // load_class('Model', 'core'); + // + // However, load_class() instantiates classes + // to cache them for later use and that prevents + // MY_Model from being an abstract class and is + // sub-optimal otherwise anyway. + if ( ! class_exists('CI_Model', FALSE)) + { + $app_path = APPPATH.'core'.DIRECTORY_SEPARATOR; + if (file_exists($app_path.'Model.php')) + { + require_once($app_path.'Model.php'); + if ( ! class_exists('CI_Model', FALSE)) + { + throw new RuntimeException($app_path."Model.php exists, but doesn't declare class CI_Model"); + } + } + elseif ( ! class_exists('CI_Model', FALSE)) + { + require_once(BASEPATH.'core'.DIRECTORY_SEPARATOR.'Model.php'); + } + + $class = config_item('subclass_prefix').'Model'; + if (file_exists($app_path.$class.'.php')) + { + require_once($app_path.$class.'.php'); + if ( ! class_exists($class, FALSE)) + { + throw new RuntimeException($app_path.$class.".php exists, but doesn't declare class ".$class); + } + } + } + + $model = ucfirst($model); + if ( ! class_exists($model, FALSE)) + { + foreach ($this->_ci_model_paths as $mod_path) + { + if ( ! file_exists($mod_path.'models/'.$path.$model.'.php')) + { + continue; + } + + require_once($mod_path.'models/'.$path.$model.'.php'); + if ( ! class_exists($model, FALSE)) + { + throw new RuntimeException($mod_path."models/".$path.$model.".php exists, but doesn't declare class ".$model); + } + + break; + } + + if ( ! class_exists($model, FALSE)) + { + throw new RuntimeException('Unable to locate the model you have specified: '.$model); + } + } + elseif ( ! is_subclass_of($model, 'CI_Model')) + { + throw new RuntimeException("Class ".$model." already exists and doesn't extend CI_Model"); + } + + $this->_ci_models[] = $name; + $CI->$name = new $model(); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Database Loader + * + * @param mixed $params Database configuration options + * @param bool $return Whether to return the database object + * @param bool $query_builder Whether to enable Query Builder + * (overrides the configuration setting) + * + * @return object|bool Database object if $return is set to TRUE, + * FALSE on failure, CI_Loader instance in any other case + */ + public function database($params = '', $return = FALSE, $query_builder = NULL) + { + // Grab the super object + $CI =& get_instance(); + + // Do we even need to load the database class? + if ($return === FALSE && $query_builder === NULL && isset($CI->db) && is_object($CI->db) && ! empty($CI->db->conn_id)) + { + return FALSE; + } + + require_once(BASEPATH.'database/DB.php'); + + if ($return === TRUE) + { + return DB($params, $query_builder); + } + + // Initialize the db variable. Needed to prevent + // reference errors with some configurations + $CI->db = ''; + + // Load the DB class + $CI->db =& DB($params, $query_builder); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Load the Database Utilities Class + * + * @param object $db Database object + * @param bool $return Whether to return the DB Utilities class object or not + * @return object + */ + public function dbutil($db = NULL, $return = FALSE) + { + $CI =& get_instance(); + + if ( ! is_object($db) OR ! ($db instanceof CI_DB)) + { + class_exists('CI_DB', FALSE) OR $this->database(); + $db =& $CI->db; + } + + require_once(BASEPATH.'database/DB_utility.php'); + require_once(BASEPATH.'database/drivers/'.$db->dbdriver.'/'.$db->dbdriver.'_utility.php'); + $class = 'CI_DB_'.$db->dbdriver.'_utility'; + + if ($return === TRUE) + { + return new $class($db); + } + + $CI->dbutil = new $class($db); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Load the Database Forge Class + * + * @param object $db Database object + * @param bool $return Whether to return the DB Forge class object or not + * @return object + */ + public function dbforge($db = NULL, $return = FALSE) + { + $CI =& get_instance(); + if ( ! is_object($db) OR ! ($db instanceof CI_DB)) + { + class_exists('CI_DB', FALSE) OR $this->database(); + $db =& $CI->db; + } + + require_once(BASEPATH.'database/DB_forge.php'); + require_once(BASEPATH.'database/drivers/'.$db->dbdriver.'/'.$db->dbdriver.'_forge.php'); + + if ( ! empty($db->subdriver)) + { + $driver_path = BASEPATH.'database/drivers/'.$db->dbdriver.'/subdrivers/'.$db->dbdriver.'_'.$db->subdriver.'_forge.php'; + if (file_exists($driver_path)) + { + require_once($driver_path); + $class = 'CI_DB_'.$db->dbdriver.'_'.$db->subdriver.'_forge'; + } + } + else + { + $class = 'CI_DB_'.$db->dbdriver.'_forge'; + } + + if ($return === TRUE) + { + return new $class($db); + } + + $CI->dbforge = new $class($db); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * View Loader + * + * Loads "view" files. + * + * @param string $view View name + * @param array $vars An associative array of data + * to be extracted for use in the view + * @param bool $return Whether to return the view output + * or leave it to the Output class + * @return object|string + */ + public function view($view, $vars = array(), $return = FALSE) + { + return $this->_ci_load(array('_ci_view' => $view, '_ci_vars' => $this->_ci_object_to_array($vars), '_ci_return' => $return)); + } + + // -------------------------------------------------------------------- + + /** + * Generic File Loader + * + * @param string $path File path + * @param bool $return Whether to return the file output + * @return object|string + */ + public function file($path, $return = FALSE) + { + return $this->_ci_load(array('_ci_path' => $path, '_ci_return' => $return)); + } + + // -------------------------------------------------------------------- + + /** + * Set Variables + * + * Once variables are set they become available within + * the controller class and its "view" files. + * + * @param array|object|string $vars + * An associative array or object containing values + * to be set, or a value's name if string + * @param string $val Value to set, only used if $vars is a string + * @return object + */ + public function vars($vars, $val = '') + { + if (is_string($vars)) + { + $vars = array($vars => $val); + } + + $vars = $this->_ci_object_to_array($vars); + + if (is_array($vars) && count($vars) > 0) + { + foreach ($vars as $key => $val) + { + $this->_ci_cached_vars[$key] = $val; + } + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Clear Cached Variables + * + * Clears the cached variables. + * + * @return CI_Loader + */ + public function clear_vars() + { + $this->_ci_cached_vars = array(); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Get Variable + * + * Check if a variable is set and retrieve it. + * + * @param string $key Variable name + * @return mixed The variable or NULL if not found + */ + public function get_var($key) + { + return isset($this->_ci_cached_vars[$key]) ? $this->_ci_cached_vars[$key] : NULL; + } + + // -------------------------------------------------------------------- + + /** + * Get Variables + * + * Retrieves all loaded variables. + * + * @return array + */ + public function get_vars() + { + return $this->_ci_cached_vars; + } + + // -------------------------------------------------------------------- + + /** + * Helper Loader + * + * @param string|string[] $helpers Helper name(s) + * @return object + */ + public function helper($helpers = array()) + { + foreach ($this->_ci_prep_filename($helpers, '_helper') as $helper) + { + if (isset($this->_ci_helpers[$helper])) + { + continue; + } + + // Is this a helper extension request? + $ext_helper = config_item('subclass_prefix').$helper; + $ext_loaded = FALSE; + foreach ($this->_ci_helper_paths as $path) + { + if (file_exists($path.'helpers/'.$ext_helper.'.php')) + { + include_once($path.'helpers/'.$ext_helper.'.php'); + $ext_loaded = TRUE; + } + } + + // If we have loaded extensions - check if the base one is here + if ($ext_loaded === TRUE) + { + $base_helper = BASEPATH.'helpers/'.$helper.'.php'; + if ( ! file_exists($base_helper)) + { + show_error('Unable to load the requested file: helpers/'.$helper.'.php'); + } + + include_once($base_helper); + $this->_ci_helpers[$helper] = TRUE; + log_message('info', 'Helper loaded: '.$helper); + continue; + } + + // No extensions found ... try loading regular helpers and/or overrides + foreach ($this->_ci_helper_paths as $path) + { + if (file_exists($path.'helpers/'.$helper.'.php')) + { + include_once($path.'helpers/'.$helper.'.php'); + + $this->_ci_helpers[$helper] = TRUE; + log_message('info', 'Helper loaded: '.$helper); + break; + } + } + + // unable to load the helper + if ( ! isset($this->_ci_helpers[$helper])) + { + show_error('Unable to load the requested file: helpers/'.$helper.'.php'); + } + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Load Helpers + * + * An alias for the helper() method in case the developer has + * written the plural form of it. + * + * @uses CI_Loader::helper() + * @param string|string[] $helpers Helper name(s) + * @return object + */ + public function helpers($helpers = array()) + { + return $this->helper($helpers); + } + + // -------------------------------------------------------------------- + + /** + * Language Loader + * + * Loads language files. + * + * @param string|string[] $files List of language file names to load + * @param string Language name + * @return object + */ + public function language($files, $lang = '') + { + get_instance()->lang->load($files, $lang); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Config Loader + * + * Loads a config file (an alias for CI_Config::load()). + * + * @uses CI_Config::load() + * @param string $file Configuration file name + * @param bool $use_sections Whether configuration values should be loaded into their own section + * @param bool $fail_gracefully Whether to just return FALSE or display an error message + * @return bool TRUE if the file was loaded correctly or FALSE on failure + */ + public function config($file, $use_sections = FALSE, $fail_gracefully = FALSE) + { + return get_instance()->config->load($file, $use_sections, $fail_gracefully); + } + + // -------------------------------------------------------------------- + + /** + * Driver Loader + * + * Loads a driver library. + * + * @param string|string[] $library Driver name(s) + * @param array $params Optional parameters to pass to the driver + * @param string $object_name An optional object name to assign to + * + * @return object|bool Object or FALSE on failure if $library is a string + * and $object_name is set. CI_Loader instance otherwise. + */ + public function driver($library, $params = NULL, $object_name = NULL) + { + if (is_array($library)) + { + foreach ($library as $key => $value) + { + if (is_int($key)) + { + $this->driver($value, $params); + } + else + { + $this->driver($key, $params, $value); + } + } + + return $this; + } + elseif (empty($library)) + { + return FALSE; + } + + if ( ! class_exists('CI_Driver_Library', FALSE)) + { + // We aren't instantiating an object here, just making the base class available + require BASEPATH.'libraries/Driver.php'; + } + + // We can save the loader some time since Drivers will *always* be in a subfolder, + // and typically identically named to the library + if ( ! strpos($library, '/')) + { + $library = ucfirst($library).'/'.$library; + } + + return $this->library($library, $params, $object_name); + } + + // -------------------------------------------------------------------- + + /** + * Add Package Path + * + * Prepends a parent path to the library, model, helper and config + * path arrays. + * + * @see CI_Loader::$_ci_library_paths + * @see CI_Loader::$_ci_model_paths + * @see CI_Loader::$_ci_helper_paths + * @see CI_Config::$_config_paths + * + * @param string $path Path to add + * @param bool $view_cascade (default: TRUE) + * @return object + */ + public function add_package_path($path, $view_cascade = TRUE) + { + $path = rtrim($path, '/').'/'; + + array_unshift($this->_ci_library_paths, $path); + array_unshift($this->_ci_model_paths, $path); + array_unshift($this->_ci_helper_paths, $path); + + $this->_ci_view_paths = array($path.'views/' => $view_cascade) + $this->_ci_view_paths; + + // Add config file path + $config =& $this->_ci_get_component('config'); + $config->_config_paths[] = $path; + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Get Package Paths + * + * Return a list of all package paths. + * + * @param bool $include_base Whether to include BASEPATH (default: FALSE) + * @return array + */ + public function get_package_paths($include_base = FALSE) + { + return ($include_base === TRUE) ? $this->_ci_library_paths : $this->_ci_model_paths; + } + + // -------------------------------------------------------------------- + + /** + * Remove Package Path + * + * Remove a path from the library, model, helper and/or config + * path arrays if it exists. If no path is provided, the most recently + * added path will be removed removed. + * + * @param string $path Path to remove + * @return object + */ + public function remove_package_path($path = '') + { + $config =& $this->_ci_get_component('config'); + + if ($path === '') + { + array_shift($this->_ci_library_paths); + array_shift($this->_ci_model_paths); + array_shift($this->_ci_helper_paths); + array_shift($this->_ci_view_paths); + array_pop($config->_config_paths); + } + else + { + $path = rtrim($path, '/').'/'; + foreach (array('_ci_library_paths', '_ci_model_paths', '_ci_helper_paths') as $var) + { + if (($key = array_search($path, $this->{$var})) !== FALSE) + { + unset($this->{$var}[$key]); + } + } + + if (isset($this->_ci_view_paths[$path.'views/'])) + { + unset($this->_ci_view_paths[$path.'views/']); + } + + if (($key = array_search($path, $config->_config_paths)) !== FALSE) + { + unset($config->_config_paths[$key]); + } + } + + // make sure the application default paths are still in the array + $this->_ci_library_paths = array_unique(array_merge($this->_ci_library_paths, array(APPPATH, BASEPATH))); + $this->_ci_helper_paths = array_unique(array_merge($this->_ci_helper_paths, array(APPPATH, BASEPATH))); + $this->_ci_model_paths = array_unique(array_merge($this->_ci_model_paths, array(APPPATH))); + $this->_ci_view_paths = array_merge($this->_ci_view_paths, array(APPPATH.'views/' => TRUE)); + $config->_config_paths = array_unique(array_merge($config->_config_paths, array(APPPATH))); + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Internal CI Data Loader + * + * Used to load views and files. + * + * Variables are prefixed with _ci_ to avoid symbol collision with + * variables made available to view files. + * + * @used-by CI_Loader::view() + * @used-by CI_Loader::file() + * @param array $_ci_data Data to load + * @return object + */ + protected function _ci_load($_ci_data) + { + // Set the default data variables + foreach (array('_ci_view', '_ci_vars', '_ci_path', '_ci_return') as $_ci_val) + { + $$_ci_val = isset($_ci_data[$_ci_val]) ? $_ci_data[$_ci_val] : FALSE; + } + + $file_exists = FALSE; + + // Set the path to the requested file + if (is_string($_ci_path) && $_ci_path !== '') + { + $_ci_x = explode('/', $_ci_path); + $_ci_file = end($_ci_x); + } + else + { + $_ci_ext = pathinfo($_ci_view, PATHINFO_EXTENSION); + $_ci_file = ($_ci_ext === '') ? $_ci_view.'.php' : $_ci_view; + + foreach ($this->_ci_view_paths as $_ci_view_file => $cascade) + { + if (file_exists($_ci_view_file.$_ci_file)) + { + $_ci_path = $_ci_view_file.$_ci_file; + $file_exists = TRUE; + break; + } + + if ( ! $cascade) + { + break; + } + } + } + + if ( ! $file_exists && ! file_exists($_ci_path)) + { + show_error('Unable to load the requested file: '.$_ci_file); + } + + // This allows anything loaded using $this->load (views, files, etc.) + // to become accessible from within the Controller and Model functions. + $_ci_CI =& get_instance(); + foreach (get_object_vars($_ci_CI) as $_ci_key => $_ci_var) + { + if ( ! isset($this->$_ci_key)) + { + $this->$_ci_key =& $_ci_CI->$_ci_key; + } + } + + /* + * Extract and cache variables + * + * You can either set variables using the dedicated $this->load->vars() + * function or via the second parameter of this function. We'll merge + * the two types and cache them so that views that are embedded within + * other views can have access to these variables. + */ + if (is_array($_ci_vars)) + { + foreach (array_keys($_ci_vars) as $key) + { + if (strncmp($key, '_ci_', 4) === 0) + { + unset($_ci_vars[$key]); + } + } + + $this->_ci_cached_vars = array_merge($this->_ci_cached_vars, $_ci_vars); + } + extract($this->_ci_cached_vars); + + /* + * Buffer the output + * + * We buffer the output for two reasons: + * 1. Speed. You get a significant speed boost. + * 2. So that the final rendered template can be post-processed by + * the output class. Why do we need post processing? For one thing, + * in order to show the elapsed page load time. Unless we can + * intercept the content right before it's sent to the browser and + * then stop the timer it won't be accurate. + */ + ob_start(); + + // If the PHP installation does not support short tags we'll + // do a little string replacement, changing the short tags + // to standard PHP echo statements. + if ( ! is_php('5.4') && ! ini_get('short_open_tag') && config_item('rewrite_short_tags') === TRUE) + { + echo eval('?>'.preg_replace('/;*\s*\?>/', '; ?>', str_replace(' $this->_ci_ob_level + 1) + { + ob_end_flush(); + } + else + { + $_ci_CI->output->append_output(ob_get_contents()); + @ob_end_clean(); + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Internal CI Library Loader + * + * @used-by CI_Loader::library() + * @uses CI_Loader::_ci_init_library() + * + * @param string $class Class name to load + * @param mixed $params Optional parameters to pass to the class constructor + * @param string $object_name Optional object name to assign to + * @return void + */ + protected function _ci_load_library($class, $params = NULL, $object_name = NULL) + { + // Get the class name, and while we're at it trim any slashes. + // The directory path can be included as part of the class name, + // but we don't want a leading slash + $class = str_replace('.php', '', trim($class, '/')); + + // Was the path included with the class name? + // We look for a slash to determine this + if (($last_slash = strrpos($class, '/')) !== FALSE) + { + // Extract the path + $subdir = substr($class, 0, ++$last_slash); + + // Get the filename from the path + $class = substr($class, $last_slash); + } + else + { + $subdir = ''; + } + + $class = ucfirst($class); + + // Is this a stock library? There are a few special conditions if so ... + if (file_exists(BASEPATH.'libraries/'.$subdir.$class.'.php')) + { + return $this->_ci_load_stock_library($class, $subdir, $params, $object_name); + } + + // Let's search for the requested library file and load it. + foreach ($this->_ci_library_paths as $path) + { + // BASEPATH has already been checked for + if ($path === BASEPATH) + { + continue; + } + + $filepath = $path.'libraries/'.$subdir.$class.'.php'; + + // Safety: Was the class already loaded by a previous call? + if (class_exists($class, FALSE)) + { + // Before we deem this to be a duplicate request, let's see + // if a custom object name is being supplied. If so, we'll + // return a new instance of the object + if ($object_name !== NULL) + { + $CI =& get_instance(); + if ( ! isset($CI->$object_name)) + { + return $this->_ci_init_library($class, '', $params, $object_name); + } + } + + log_message('debug', $class.' class already loaded. Second attempt ignored.'); + return; + } + // Does the file exist? No? Bummer... + elseif ( ! file_exists($filepath)) + { + continue; + } + + include_once($filepath); + return $this->_ci_init_library($class, '', $params, $object_name); + } + + // One last attempt. Maybe the library is in a subdirectory, but it wasn't specified? + if ($subdir === '') + { + return $this->_ci_load_library($class.'/'.$class, $params, $object_name); + } + + // If we got this far we were unable to find the requested class. + log_message('error', 'Unable to load the requested class: '.$class); + show_error('Unable to load the requested class: '.$class); + } + + // -------------------------------------------------------------------- + + /** + * Internal CI Stock Library Loader + * + * @used-by CI_Loader::_ci_load_library() + * @uses CI_Loader::_ci_init_library() + * + * @param string $library_name Library name to load + * @param string $file_path Path to the library filename, relative to libraries/ + * @param mixed $params Optional parameters to pass to the class constructor + * @param string $object_name Optional object name to assign to + * @return void + */ + protected function _ci_load_stock_library($library_name, $file_path, $params, $object_name) + { + $prefix = 'CI_'; + + if (class_exists($prefix.$library_name, FALSE)) + { + if (class_exists(config_item('subclass_prefix').$library_name, FALSE)) + { + $prefix = config_item('subclass_prefix'); + } + + // Before we deem this to be a duplicate request, let's see + // if a custom object name is being supplied. If so, we'll + // return a new instance of the object + if ($object_name !== NULL) + { + $CI =& get_instance(); + if ( ! isset($CI->$object_name)) + { + return $this->_ci_init_library($library_name, $prefix, $params, $object_name); + } + } + + log_message('debug', $library_name.' class already loaded. Second attempt ignored.'); + return; + } + + $paths = $this->_ci_library_paths; + array_pop($paths); // BASEPATH + array_pop($paths); // APPPATH (needs to be the first path checked) + array_unshift($paths, APPPATH); + + foreach ($paths as $path) + { + if (file_exists($path = $path.'libraries/'.$file_path.$library_name.'.php')) + { + // Override + include_once($path); + if (class_exists($prefix.$library_name, FALSE)) + { + return $this->_ci_init_library($library_name, $prefix, $params, $object_name); + } + else + { + log_message('debug', $path.' exists, but does not declare '.$prefix.$library_name); + } + } + } + + include_once(BASEPATH.'libraries/'.$file_path.$library_name.'.php'); + + // Check for extensions + $subclass = config_item('subclass_prefix').$library_name; + foreach ($paths as $path) + { + if (file_exists($path = $path.'libraries/'.$file_path.$subclass.'.php')) + { + include_once($path); + if (class_exists($subclass, FALSE)) + { + $prefix = config_item('subclass_prefix'); + break; + } + else + { + log_message('debug', $path.' exists, but does not declare '.$subclass); + } + } + } + + return $this->_ci_init_library($library_name, $prefix, $params, $object_name); + } + + // -------------------------------------------------------------------- + + /** + * Internal CI Library Instantiator + * + * @used-by CI_Loader::_ci_load_stock_library() + * @used-by CI_Loader::_ci_load_library() + * + * @param string $class Class name + * @param string $prefix Class name prefix + * @param array|null|bool $config Optional configuration to pass to the class constructor: + * FALSE to skip; + * NULL to search in config paths; + * array containing configuration data + * @param string $object_name Optional object name to assign to + * @return void + */ + protected function _ci_init_library($class, $prefix, $config = FALSE, $object_name = NULL) + { + // Is there an associated config file for this class? Note: these should always be lowercase + if ($config === NULL) + { + // Fetch the config paths containing any package paths + $config_component = $this->_ci_get_component('config'); + + if (is_array($config_component->_config_paths)) + { + $found = FALSE; + foreach ($config_component->_config_paths as $path) + { + // We test for both uppercase and lowercase, for servers that + // are case-sensitive with regard to file names. Load global first, + // override with environment next + if (file_exists($path.'config/'.strtolower($class).'.php')) + { + include($path.'config/'.strtolower($class).'.php'); + $found = TRUE; + } + elseif (file_exists($path.'config/'.ucfirst(strtolower($class)).'.php')) + { + include($path.'config/'.ucfirst(strtolower($class)).'.php'); + $found = TRUE; + } + + if (file_exists($path.'config/'.ENVIRONMENT.'/'.strtolower($class).'.php')) + { + include($path.'config/'.ENVIRONMENT.'/'.strtolower($class).'.php'); + $found = TRUE; + } + elseif (file_exists($path.'config/'.ENVIRONMENT.'/'.ucfirst(strtolower($class)).'.php')) + { + include($path.'config/'.ENVIRONMENT.'/'.ucfirst(strtolower($class)).'.php'); + $found = TRUE; + } + + // Break on the first found configuration, thus package + // files are not overridden by default paths + if ($found === TRUE) + { + break; + } + } + } + } + + $class_name = $prefix.$class; + + // Is the class name valid? + if ( ! class_exists($class_name, FALSE)) + { + log_message('error', 'Non-existent class: '.$class_name); + show_error('Non-existent class: '.$class_name); + } + + // Set the variable name we will assign the class to + // Was a custom class name supplied? If so we'll use it + if (empty($object_name)) + { + $object_name = strtolower($class); + if (isset($this->_ci_varmap[$object_name])) + { + $object_name = $this->_ci_varmap[$object_name]; + } + } + + // Don't overwrite existing properties + $CI =& get_instance(); + if (isset($CI->$object_name)) + { + if ($CI->$object_name instanceof $class_name) + { + log_message('debug', $class_name." has already been instantiated as '".$object_name."'. Second attempt aborted."); + return; + } + + show_error("Resource '".$object_name."' already exists and is not a ".$class_name." instance."); + } + + // Save the class name and object name + $this->_ci_classes[$object_name] = $class; + + // Instantiate the class + $CI->$object_name = isset($config) + ? new $class_name($config) + : new $class_name(); + } + + // -------------------------------------------------------------------- + + /** + * CI Autoloader + * + * Loads component listed in the config/autoload.php file. + * + * @used-by CI_Loader::initialize() + * @return void + */ + protected function _ci_autoloader() + { + if (file_exists(APPPATH.'config/autoload.php')) + { + include(APPPATH.'config/autoload.php'); + } + + if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/autoload.php')) + { + include(APPPATH.'config/'.ENVIRONMENT.'/autoload.php'); + } + + if ( ! isset($autoload)) + { + return; + } + + // Autoload packages + if (isset($autoload['packages'])) + { + foreach ($autoload['packages'] as $package_path) + { + $this->add_package_path($package_path); + } + } + + // Load any custom config file + if (count($autoload['config']) > 0) + { + foreach ($autoload['config'] as $val) + { + $this->config($val); + } + } + + // Autoload helpers and languages + foreach (array('helper', 'language') as $type) + { + if (isset($autoload[$type]) && count($autoload[$type]) > 0) + { + $this->$type($autoload[$type]); + } + } + + // Autoload drivers + if (isset($autoload['drivers'])) + { + $this->driver($autoload['drivers']); + } + + // Load libraries + if (isset($autoload['libraries']) && count($autoload['libraries']) > 0) + { + // Load the database driver. + if (in_array('database', $autoload['libraries'])) + { + $this->database(); + $autoload['libraries'] = array_diff($autoload['libraries'], array('database')); + } + + // Load all other libraries + $this->library($autoload['libraries']); + } + + // Autoload models + if (isset($autoload['model'])) + { + $this->model($autoload['model']); + } + } + + // -------------------------------------------------------------------- + + /** + * CI Object to Array translator + * + * Takes an object as input and converts the class variables to + * an associative array with key/value pairs. + * + * @param object $object Object data to translate + * @return array + */ + protected function _ci_object_to_array($object) + { + return is_object($object) ? get_object_vars($object) : $object; + } + + // -------------------------------------------------------------------- + + /** + * CI Component getter + * + * Get a reference to a specific library or model. + * + * @param string $component Component name + * @return bool + */ + protected function &_ci_get_component($component) + { + $CI =& get_instance(); + return $CI->$component; + } + + // -------------------------------------------------------------------- + + /** + * Prep filename + * + * This function prepares filenames of various items to + * make their loading more reliable. + * + * @param string|string[] $filename Filename(s) + * @param string $extension Filename extension + * @return array + */ + protected function _ci_prep_filename($filename, $extension) + { + if ( ! is_array($filename)) + { + return array(strtolower(str_replace(array($extension, '.php'), '', $filename).$extension)); + } + else + { + foreach ($filename as $key => $val) + { + $filename[$key] = strtolower(str_replace(array($extension, '.php'), '', $val).$extension); + } + + return $filename; + } + } + +} diff --git a/system/core/Log.php b/system/core/Log.php new file mode 100644 index 0000000..cf6c75a --- /dev/null +++ b/system/core/Log.php @@ -0,0 +1,296 @@ + 1, 'DEBUG' => 2, 'INFO' => 3, 'ALL' => 4); + + /** + * mbstring.func_override flag + * + * @var bool + */ + protected static $func_override; + + // -------------------------------------------------------------------- + + /** + * Class constructor + * + * @return void + */ + public function __construct() + { + $config =& get_config(); + + isset(self::$func_override) OR self::$func_override = (extension_loaded('mbstring') && ini_get('mbstring.func_override')); + + $this->_log_path = ($config['log_path'] !== '') ? $config['log_path'] : APPPATH.'logs/'; + $this->_file_ext = (isset($config['log_file_extension']) && $config['log_file_extension'] !== '') + ? ltrim($config['log_file_extension'], '.') : 'php'; + + file_exists($this->_log_path) OR mkdir($this->_log_path, 0755, TRUE); + + if ( ! is_dir($this->_log_path) OR ! is_really_writable($this->_log_path)) + { + $this->_enabled = FALSE; + } + + if (is_numeric($config['log_threshold'])) + { + $this->_threshold = (int) $config['log_threshold']; + } + elseif (is_array($config['log_threshold'])) + { + $this->_threshold = 0; + $this->_threshold_array = array_flip($config['log_threshold']); + } + + if ( ! empty($config['log_date_format'])) + { + $this->_date_fmt = $config['log_date_format']; + } + + if ( ! empty($config['log_file_permissions']) && is_int($config['log_file_permissions'])) + { + $this->_file_permissions = $config['log_file_permissions']; + } + } + + // -------------------------------------------------------------------- + + /** + * Write Log File + * + * Generally this function will be called using the global log_message() function + * + * @param string $level The error level: 'error', 'debug' or 'info' + * @param string $msg The error message + * @return bool + */ + public function write_log($level, $msg) + { + if ($this->_enabled === FALSE) + { + return FALSE; + } + + $level = strtoupper($level); + + if (( ! isset($this->_levels[$level]) OR ($this->_levels[$level] > $this->_threshold)) + && ! isset($this->_threshold_array[$this->_levels[$level]])) + { + return FALSE; + } + + $filepath = $this->_log_path.'log-'.date('Y-m-d').'.'.$this->_file_ext; + $message = ''; + + if ( ! file_exists($filepath)) + { + $newfile = TRUE; + // Only add protection to php files + if ($this->_file_ext === 'php') + { + $message .= "\n\n"; + } + } + + if ( ! $fp = @fopen($filepath, 'ab')) + { + return FALSE; + } + + flock($fp, LOCK_EX); + + // Instantiating DateTime with microseconds appended to initial date is needed for proper support of this format + if (strpos($this->_date_fmt, 'u') !== FALSE) + { + $microtime_full = microtime(TRUE); + $microtime_short = sprintf("%06d", ($microtime_full - floor($microtime_full)) * 1000000); + $date = new DateTime(date('Y-m-d H:i:s.'.$microtime_short, $microtime_full)); + $date = $date->format($this->_date_fmt); + } + else + { + $date = date($this->_date_fmt); + } + + $message .= $this->_format_line($level, $date, $msg); + + for ($written = 0, $length = self::strlen($message); $written < $length; $written += $result) + { + if (($result = fwrite($fp, self::substr($message, $written))) === FALSE) + { + break; + } + } + + flock($fp, LOCK_UN); + fclose($fp); + + if (isset($newfile) && $newfile === TRUE) + { + chmod($filepath, $this->_file_permissions); + } + + return is_int($result); + } + + // -------------------------------------------------------------------- + + /** + * Format the log line. + * + * This is for extensibility of log formatting + * If you want to change the log format, extend the CI_Log class and override this method + * + * @param string $level The error level + * @param string $date Formatted date string + * @param string $message The log message + * @return string Formatted log line with a new line character '\n' at the end + */ + protected function _format_line($level, $date, $message) + { + return $level.' - '.$date.' --> '.$message."\n"; + } + + // -------------------------------------------------------------------- + + /** + * Byte-safe strlen() + * + * @param string $str + * @return int + */ + protected static function strlen($str) + { + return (self::$func_override) + ? mb_strlen($str, '8bit') + : strlen($str); + } + + // -------------------------------------------------------------------- + + /** + * Byte-safe substr() + * + * @param string $str + * @param int $start + * @param int $length + * @return string + */ + protected static function substr($str, $start, $length = NULL) + { + if (self::$func_override) + { + // mb_substr($str, $start, null, '8bit') returns an empty + // string on PHP 5.3 + isset($length) OR $length = ($start >= 0 ? self::strlen($str) - $start : -$start); + return mb_substr($str, $start, $length, '8bit'); + } + + return isset($length) + ? substr($str, $start, $length) + : substr($str, $start); + } +} diff --git a/system/core/Model.php b/system/core/Model.php new file mode 100644 index 0000000..941881a --- /dev/null +++ b/system/core/Model.php @@ -0,0 +1,80 @@ +$key; + } + +} diff --git a/system/core/Output.php b/system/core/Output.php new file mode 100644 index 0000000..cf6510f --- /dev/null +++ b/system/core/Output.php @@ -0,0 +1,848 @@ +_zlib_oc = (bool) ini_get('zlib.output_compression'); + $this->_compress_output = ( + $this->_zlib_oc === FALSE + && config_item('compress_output') === TRUE + && extension_loaded('zlib') + ); + + isset(self::$func_override) OR self::$func_override = (extension_loaded('mbstring') && ini_get('mbstring.func_override')); + + // Get mime types for later + $this->mimes =& get_mimes(); + + log_message('info', 'Output Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Get Output + * + * Returns the current output string. + * + * @return string + */ + public function get_output() + { + return $this->final_output; + } + + // -------------------------------------------------------------------- + + /** + * Set Output + * + * Sets the output string. + * + * @param string $output Output data + * @return CI_Output + */ + public function set_output($output) + { + $this->final_output = $output; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Append Output + * + * Appends data onto the output string. + * + * @param string $output Data to append + * @return CI_Output + */ + public function append_output($output) + { + $this->final_output .= $output; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Set Header + * + * Lets you set a server header which will be sent with the final output. + * + * Note: If a file is cached, headers will not be sent. + * @todo We need to figure out how to permit headers to be cached. + * + * @param string $header Header + * @param bool $replace Whether to replace the old header value, if already set + * @return CI_Output + */ + public function set_header($header, $replace = TRUE) + { + // If zlib.output_compression is enabled it will compress the output, + // but it will not modify the content-length header to compensate for + // the reduction, causing the browser to hang waiting for more data. + // We'll just skip content-length in those cases. + if ($this->_zlib_oc && strncasecmp($header, 'content-length', 14) === 0) + { + return $this; + } + + $this->headers[] = array($header, $replace); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Set Content-Type Header + * + * @param string $mime_type Extension of the file we're outputting + * @param string $charset Character set (default: NULL) + * @return CI_Output + */ + public function set_content_type($mime_type, $charset = NULL) + { + if (strpos($mime_type, '/') === FALSE) + { + $extension = ltrim($mime_type, '.'); + + // Is this extension supported? + if (isset($this->mimes[$extension])) + { + $mime_type =& $this->mimes[$extension]; + + if (is_array($mime_type)) + { + $mime_type = current($mime_type); + } + } + } + + $this->mime_type = $mime_type; + + if (empty($charset)) + { + $charset = config_item('charset'); + } + + $header = 'Content-Type: '.$mime_type + .(empty($charset) ? '' : '; charset='.$charset); + + $this->headers[] = array($header, TRUE); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Get Current Content-Type Header + * + * @return string 'text/html', if not already set + */ + public function get_content_type() + { + for ($i = 0, $c = count($this->headers); $i < $c; $i++) + { + if (sscanf($this->headers[$i][0], 'Content-Type: %[^;]', $content_type) === 1) + { + return $content_type; + } + } + + return 'text/html'; + } + + // -------------------------------------------------------------------- + + /** + * Get Header + * + * @param string $header + * @return string + */ + public function get_header($header) + { + // Combine headers already sent with our batched headers + $headers = array_merge( + // We only need [x][0] from our multi-dimensional array + array_map('array_shift', $this->headers), + headers_list() + ); + + if (empty($headers) OR empty($header)) + { + return NULL; + } + + for ($i = 0, $c = count($headers); $i < $c; $i++) + { + if (strncasecmp($header, $headers[$i], $l = self::strlen($header)) === 0) + { + return trim(self::substr($headers[$i], $l+1)); + } + } + + return NULL; + } + + // -------------------------------------------------------------------- + + /** + * Set HTTP Status Header + * + * As of version 1.7.2, this is an alias for common function + * set_status_header(). + * + * @param int $code Status code (default: 200) + * @param string $text Optional message + * @return CI_Output + */ + public function set_status_header($code = 200, $text = '') + { + set_status_header($code, $text); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Enable/disable Profiler + * + * @param bool $val TRUE to enable or FALSE to disable + * @return CI_Output + */ + public function enable_profiler($val = TRUE) + { + $this->enable_profiler = is_bool($val) ? $val : TRUE; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Set Profiler Sections + * + * Allows override of default/config settings for + * Profiler section display. + * + * @param array $sections Profiler sections + * @return CI_Output + */ + public function set_profiler_sections($sections) + { + if (isset($sections['query_toggle_count'])) + { + $this->_profiler_sections['query_toggle_count'] = (int) $sections['query_toggle_count']; + unset($sections['query_toggle_count']); + } + + foreach ($sections as $section => $enable) + { + $this->_profiler_sections[$section] = ($enable !== FALSE); + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Set Cache + * + * @param int $time Cache expiration time in minutes + * @return CI_Output + */ + public function cache($time) + { + $this->cache_expiration = is_numeric($time) ? $time : 0; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Display Output + * + * Processes and sends finalized output data to the browser along + * with any server headers and profile data. It also stops benchmark + * timers so the page rendering speed and memory usage can be shown. + * + * Note: All "view" data is automatically put into $this->final_output + * by controller class. + * + * @uses CI_Output::$final_output + * @param string $output Output data override + * @return void + */ + public function _display($output = '') + { + // Note: We use load_class() because we can't use $CI =& get_instance() + // since this function is sometimes called by the caching mechanism, + // which happens before the CI super object is available. + $BM =& load_class('Benchmark', 'core'); + $CFG =& load_class('Config', 'core'); + + // Grab the super object if we can. + if (class_exists('CI_Controller', FALSE)) + { + $CI =& get_instance(); + } + + // -------------------------------------------------------------------- + + // Set the output data + if ($output === '') + { + $output =& $this->final_output; + } + + // -------------------------------------------------------------------- + + // Do we need to write a cache file? Only if the controller does not have its + // own _output() method and we are not dealing with a cache file, which we + // can determine by the existence of the $CI object above + if ($this->cache_expiration > 0 && isset($CI) && ! method_exists($CI, '_output')) + { + $this->_write_cache($output); + } + + // -------------------------------------------------------------------- + + // Parse out the elapsed time and memory usage, + // then swap the pseudo-variables with the data + + $elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end'); + + if ($this->parse_exec_vars === TRUE) + { + $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB'; + $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output); + } + + // -------------------------------------------------------------------- + + // Is compression requested? + if (isset($CI) // This means that we're not serving a cache file, if we were, it would already be compressed + && $this->_compress_output === TRUE + && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) + { + ob_start('ob_gzhandler'); + } + + // -------------------------------------------------------------------- + + // Are there any server headers to send? + if (count($this->headers) > 0) + { + foreach ($this->headers as $header) + { + @header($header[0], $header[1]); + } + } + + // -------------------------------------------------------------------- + + // Does the $CI object exist? + // If not we know we are dealing with a cache file so we'll + // simply echo out the data and exit. + if ( ! isset($CI)) + { + if ($this->_compress_output === TRUE) + { + if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) + { + header('Content-Encoding: gzip'); + header('Content-Length: '.self::strlen($output)); + } + else + { + // User agent doesn't support gzip compression, + // so we'll have to decompress our cache + $output = gzinflate(self::substr($output, 10, -8)); + } + } + + echo $output; + log_message('info', 'Final output sent to browser'); + log_message('debug', 'Total execution time: '.$elapsed); + return; + } + + // -------------------------------------------------------------------- + + // Do we need to generate profile data? + // If so, load the Profile class and run it. + if ($this->enable_profiler === TRUE) + { + $CI->load->library('profiler'); + if ( ! empty($this->_profiler_sections)) + { + $CI->profiler->set_sections($this->_profiler_sections); + } + + // If the output data contains closing and tags + // we will remove them and add them back after we insert the profile data + $output = preg_replace('|.*?|is', '', $output, -1, $count).$CI->profiler->run(); + if ($count > 0) + { + $output .= ''; + } + } + + // Does the controller contain a function named _output()? + // If so send the output there. Otherwise, echo it. + if (method_exists($CI, '_output')) + { + $CI->_output($output); + } + else + { + echo $output; // Send it to the browser! + } + + log_message('info', 'Final output sent to browser'); + log_message('debug', 'Total execution time: '.$elapsed); + } + + // -------------------------------------------------------------------- + + /** + * Write Cache + * + * @param string $output Output data to cache + * @return void + */ + public function _write_cache($output) + { + $CI =& get_instance(); + $path = $CI->config->item('cache_path'); + $cache_path = ($path === '') ? APPPATH.'cache/' : $path; + + if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path)) + { + log_message('error', 'Unable to write cache file: '.$cache_path); + return; + } + + $uri = $CI->config->item('base_url') + .$CI->config->item('index_page') + .$CI->uri->uri_string(); + + if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) + { + if (is_array($cache_query_string)) + { + $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); + } + else + { + $uri .= '?'.$_SERVER['QUERY_STRING']; + } + } + + $cache_path .= md5($uri); + + if ( ! $fp = @fopen($cache_path, 'w+b')) + { + log_message('error', 'Unable to write cache file: '.$cache_path); + return; + } + + if (flock($fp, LOCK_EX)) + { + // If output compression is enabled, compress the cache + // itself, so that we don't have to do that each time + // we're serving it + if ($this->_compress_output === TRUE) + { + $output = gzencode($output); + + if ($this->get_header('content-type') === NULL) + { + $this->set_content_type($this->mime_type); + } + } + + $expire = time() + ($this->cache_expiration * 60); + + // Put together our serialized info. + $cache_info = serialize(array( + 'expire' => $expire, + 'headers' => $this->headers + )); + + $output = $cache_info.'ENDCI--->'.$output; + + for ($written = 0, $length = self::strlen($output); $written < $length; $written += $result) + { + if (($result = fwrite($fp, self::substr($output, $written))) === FALSE) + { + break; + } + } + + flock($fp, LOCK_UN); + } + else + { + log_message('error', 'Unable to secure a file lock for file at: '.$cache_path); + return; + } + + fclose($fp); + + if (is_int($result)) + { + chmod($cache_path, 0640); + log_message('debug', 'Cache file written: '.$cache_path); + + // Send HTTP cache-control headers to browser to match file cache settings. + $this->set_cache_header($_SERVER['REQUEST_TIME'], $expire); + } + else + { + @unlink($cache_path); + log_message('error', 'Unable to write the complete cache content at: '.$cache_path); + } + } + + // -------------------------------------------------------------------- + + /** + * Update/serve cached output + * + * @uses CI_Config + * @uses CI_URI + * + * @param object &$CFG CI_Config class instance + * @param object &$URI CI_URI class instance + * @return bool TRUE on success or FALSE on failure + */ + public function _display_cache(&$CFG, &$URI) + { + $cache_path = ($CFG->item('cache_path') === '') ? APPPATH.'cache/' : $CFG->item('cache_path'); + + // Build the file path. The file name is an MD5 hash of the full URI + $uri = $CFG->item('base_url').$CFG->item('index_page').$URI->uri_string; + + if (($cache_query_string = $CFG->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) + { + if (is_array($cache_query_string)) + { + $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); + } + else + { + $uri .= '?'.$_SERVER['QUERY_STRING']; + } + } + + $filepath = $cache_path.md5($uri); + + if ( ! file_exists($filepath) OR ! $fp = @fopen($filepath, 'rb')) + { + return FALSE; + } + + flock($fp, LOCK_SH); + + $cache = (filesize($filepath) > 0) ? fread($fp, filesize($filepath)) : ''; + + flock($fp, LOCK_UN); + fclose($fp); + + // Look for embedded serialized file info. + if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match)) + { + return FALSE; + } + + $cache_info = unserialize($match[1]); + $expire = $cache_info['expire']; + + $last_modified = filemtime($filepath); + + // Has the file expired? + if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path)) + { + // If so we'll delete it. + @unlink($filepath); + log_message('debug', 'Cache file has expired. File deleted.'); + return FALSE; + } + else + { + // Or else send the HTTP cache control headers. + $this->set_cache_header($last_modified, $expire); + } + + // Add headers from cache file. + foreach ($cache_info['headers'] as $header) + { + $this->set_header($header[0], $header[1]); + } + + // Display the cache + $this->_display(self::substr($cache, self::strlen($match[0]))); + log_message('debug', 'Cache file is current. Sending it to browser.'); + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Delete cache + * + * @param string $uri URI string + * @return bool + */ + public function delete_cache($uri = '') + { + $CI =& get_instance(); + $cache_path = $CI->config->item('cache_path'); + if ($cache_path === '') + { + $cache_path = APPPATH.'cache/'; + } + + if ( ! is_dir($cache_path)) + { + log_message('error', 'Unable to find cache path: '.$cache_path); + return FALSE; + } + + if (empty($uri)) + { + $uri = $CI->uri->uri_string(); + + if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) + { + if (is_array($cache_query_string)) + { + $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); + } + else + { + $uri .= '?'.$_SERVER['QUERY_STRING']; + } + } + } + + $cache_path .= md5($CI->config->item('base_url').$CI->config->item('index_page').ltrim($uri, '/')); + + if ( ! @unlink($cache_path)) + { + log_message('error', 'Unable to delete cache file for '.$uri); + return FALSE; + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Set Cache Header + * + * Set the HTTP headers to match the server-side file cache settings + * in order to reduce bandwidth. + * + * @param int $last_modified Timestamp of when the page was last modified + * @param int $expiration Timestamp of when should the requested page expire from cache + * @return void + */ + public function set_cache_header($last_modified, $expiration) + { + $max_age = $expiration - $_SERVER['REQUEST_TIME']; + + if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) + { + $this->set_status_header(304); + exit; + } + else + { + header('Pragma: public'); + header('Cache-Control: max-age='.$max_age.', public'); + header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT'); + header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT'); + } + } + + // -------------------------------------------------------------------- + + /** + * Byte-safe strlen() + * + * @param string $str + * @return int + */ + protected static function strlen($str) + { + return (self::$func_override) + ? mb_strlen($str, '8bit') + : strlen($str); + } + + // -------------------------------------------------------------------- + + /** + * Byte-safe substr() + * + * @param string $str + * @param int $start + * @param int $length + * @return string + */ + protected static function substr($str, $start, $length = NULL) + { + if (self::$func_override) + { + // mb_substr($str, $start, null, '8bit') returns an empty + // string on PHP 5.3 + isset($length) OR $length = ($start >= 0 ? self::strlen($str) - $start : -$start); + return mb_substr($str, $start, $length, '8bit'); + } + + return isset($length) + ? substr($str, $start, $length) + : substr($str, $start); + } +} diff --git a/system/core/Router.php b/system/core/Router.php new file mode 100644 index 0000000..045d366 --- /dev/null +++ b/system/core/Router.php @@ -0,0 +1,515 @@ +config =& load_class('Config', 'core'); + $this->uri =& load_class('URI', 'core'); + + $this->enable_query_strings = ( ! is_cli() && $this->config->item('enable_query_strings') === TRUE); + + // If a directory override is configured, it has to be set before any dynamic routing logic + is_array($routing) && isset($routing['directory']) && $this->set_directory($routing['directory']); + $this->_set_routing(); + + // Set any routing overrides that may exist in the main index file + if (is_array($routing)) + { + empty($routing['controller']) OR $this->set_class($routing['controller']); + empty($routing['function']) OR $this->set_method($routing['function']); + } + + log_message('info', 'Router Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Set route mapping + * + * Determines what should be served based on the URI request, + * as well as any "routes" that have been set in the routing config file. + * + * @return void + */ + protected function _set_routing() + { + // Load the routes.php file. It would be great if we could + // skip this for enable_query_strings = TRUE, but then + // default_controller would be empty ... + if (file_exists(APPPATH.'config/routes.php')) + { + include(APPPATH.'config/routes.php'); + } + + if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/routes.php')) + { + include(APPPATH.'config/'.ENVIRONMENT.'/routes.php'); + } + + // Validate & get reserved routes + if (isset($route) && is_array($route)) + { + isset($route['default_controller']) && $this->default_controller = $route['default_controller']; + isset($route['translate_uri_dashes']) && $this->translate_uri_dashes = $route['translate_uri_dashes']; + unset($route['default_controller'], $route['translate_uri_dashes']); + $this->routes = $route; + } + + // Are query strings enabled in the config file? Normally CI doesn't utilize query strings + // since URI segments are more search-engine friendly, but they can optionally be used. + // If this feature is enabled, we will gather the directory/class/method a little differently + if ($this->enable_query_strings) + { + // If the directory is set at this time, it means an override exists, so skip the checks + if ( ! isset($this->directory)) + { + $_d = $this->config->item('directory_trigger'); + $_d = isset($_GET[$_d]) ? trim($_GET[$_d], " \t\n\r\0\x0B/") : ''; + + if ($_d !== '') + { + $this->uri->filter_uri($_d); + $this->set_directory($_d); + } + } + + $_c = trim($this->config->item('controller_trigger')); + if ( ! empty($_GET[$_c])) + { + $this->uri->filter_uri($_GET[$_c]); + $this->set_class($_GET[$_c]); + + $_f = trim($this->config->item('function_trigger')); + if ( ! empty($_GET[$_f])) + { + $this->uri->filter_uri($_GET[$_f]); + $this->set_method($_GET[$_f]); + } + + $this->uri->rsegments = array( + 1 => $this->class, + 2 => $this->method + ); + } + else + { + $this->_set_default_controller(); + } + + // Routing rules don't apply to query strings and we don't need to detect + // directories, so we're done here + return; + } + + // Is there anything to parse? + if ($this->uri->uri_string !== '') + { + $this->_parse_routes(); + } + else + { + $this->_set_default_controller(); + } + } + + // -------------------------------------------------------------------- + + /** + * Set request route + * + * Takes an array of URI segments as input and sets the class/method + * to be called. + * + * @used-by CI_Router::_parse_routes() + * @param array $segments URI segments + * @return void + */ + protected function _set_request($segments = array()) + { + $segments = $this->_validate_request($segments); + // If we don't have any segments left - try the default controller; + // WARNING: Directories get shifted out of the segments array! + if (empty($segments)) + { + $this->_set_default_controller(); + return; + } + + if ($this->translate_uri_dashes === TRUE) + { + $segments[0] = str_replace('-', '_', $segments[0]); + if (isset($segments[1])) + { + $segments[1] = str_replace('-', '_', $segments[1]); + } + } + + $this->set_class($segments[0]); + if (isset($segments[1])) + { + $this->set_method($segments[1]); + } + else + { + $segments[1] = 'index'; + } + + array_unshift($segments, NULL); + unset($segments[0]); + $this->uri->rsegments = $segments; + } + + // -------------------------------------------------------------------- + + /** + * Set default controller + * + * @return void + */ + protected function _set_default_controller() + { + if (empty($this->default_controller)) + { + show_error('Unable to determine what should be displayed. A default route has not been specified in the routing file.'); + } + + // Is the method being specified? + if (sscanf($this->default_controller, '%[^/]/%s', $class, $method) !== 2) + { + $method = 'index'; + } + + if ( ! file_exists(APPPATH.'controllers/'.$this->directory.ucfirst($class).'.php')) + { + // This will trigger 404 later + return; + } + + $this->set_class($class); + $this->set_method($method); + + // Assign routed segments, index starting from 1 + $this->uri->rsegments = array( + 1 => $class, + 2 => $method + ); + + log_message('debug', 'No URI present. Default controller set.'); + } + + // -------------------------------------------------------------------- + + /** + * Validate request + * + * Attempts validate the URI request and determine the controller path. + * + * @used-by CI_Router::_set_request() + * @param array $segments URI segments + * @return mixed URI segments + */ + protected function _validate_request($segments) + { + $c = count($segments); + $directory_override = isset($this->directory); + + // Loop through our segments and return as soon as a controller + // is found or when such a directory doesn't exist + while ($c-- > 0) + { + $test = $this->directory + .ucfirst($this->translate_uri_dashes === TRUE ? str_replace('-', '_', $segments[0]) : $segments[0]); + + if ( ! file_exists(APPPATH.'controllers/'.$test.'.php') + && $directory_override === FALSE + && is_dir(APPPATH.'controllers/'.$this->directory.$segments[0]) + ) + { + $this->set_directory(array_shift($segments), TRUE); + continue; + } + + return $segments; + } + + // This means that all segments were actually directories + return $segments; + } + + // -------------------------------------------------------------------- + + /** + * Parse Routes + * + * Matches any routes that may exist in the config/routes.php file + * against the URI to determine if the class/method need to be remapped. + * + * @return void + */ + protected function _parse_routes() + { + // Turn the segment array into a URI string + $uri = implode('/', $this->uri->segments); + + // Get HTTP verb + $http_verb = isset($_SERVER['REQUEST_METHOD']) ? strtolower($_SERVER['REQUEST_METHOD']) : 'cli'; + + // Loop through the route array looking for wildcards + foreach ($this->routes as $key => $val) + { + // Check if route format is using HTTP verbs + if (is_array($val)) + { + $val = array_change_key_case($val, CASE_LOWER); + if (isset($val[$http_verb])) + { + $val = $val[$http_verb]; + } + else + { + continue; + } + } + + // Convert wildcards to RegEx + $key = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $key); + + // Does the RegEx match? + if (preg_match('#^'.$key.'$#', $uri, $matches)) + { + // Are we using callbacks to process back-references? + if ( ! is_string($val) && is_callable($val)) + { + // Remove the original string from the matches array. + array_shift($matches); + + // Execute the callback using the values in matches as its parameters. + $val = call_user_func_array($val, $matches); + } + // Are we using the default routing method for back-references? + elseif (strpos($val, '$') !== FALSE && strpos($key, '(') !== FALSE) + { + $val = preg_replace('#^'.$key.'$#', $val, $uri); + } + + $this->_set_request(explode('/', $val)); + return; + } + } + + // If we got this far it means we didn't encounter a + // matching route so we'll set the site default route + $this->_set_request(array_values($this->uri->segments)); + } + + // -------------------------------------------------------------------- + + /** + * Set class name + * + * @param string $class Class name + * @return void + */ + public function set_class($class) + { + $this->class = str_replace(array('/', '.'), '', $class); + } + + // -------------------------------------------------------------------- + + /** + * Fetch the current class + * + * @deprecated 3.0.0 Read the 'class' property instead + * @return string + */ + public function fetch_class() + { + return $this->class; + } + + // -------------------------------------------------------------------- + + /** + * Set method name + * + * @param string $method Method name + * @return void + */ + public function set_method($method) + { + $this->method = $method; + } + + // -------------------------------------------------------------------- + + /** + * Fetch the current method + * + * @deprecated 3.0.0 Read the 'method' property instead + * @return string + */ + public function fetch_method() + { + return $this->method; + } + + // -------------------------------------------------------------------- + + /** + * Set directory name + * + * @param string $dir Directory name + * @param bool $append Whether we're appending rather than setting the full value + * @return void + */ + public function set_directory($dir, $append = FALSE) + { + if ($append !== TRUE OR empty($this->directory)) + { + $this->directory = str_replace('.', '', trim($dir, '/')).'/'; + } + else + { + $this->directory .= str_replace('.', '', trim($dir, '/')).'/'; + } + } + + // -------------------------------------------------------------------- + + /** + * Fetch directory + * + * Feches the sub-directory (if any) that contains the requested + * controller class. + * + * @deprecated 3.0.0 Read the 'directory' property instead + * @return string + */ + public function fetch_directory() + { + return $this->directory; + } + +} diff --git a/system/core/Security.php b/system/core/Security.php new file mode 100644 index 0000000..d0308c5 --- /dev/null +++ b/system/core/Security.php @@ -0,0 +1,1078 @@ +', '<', '>', + "'", '"', '&', '$', '#', + '{', '}', '[', ']', '=', + ';', '?', '%20', '%22', + '%3c', // < + '%253c', // < + '%3e', // > + '%0e', // > + '%28', // ( + '%29', // ) + '%2528', // ( + '%26', // & + '%24', // $ + '%3f', // ? + '%3b', // ; + '%3d' // = + ); + + /** + * Character set + * + * Will be overridden by the constructor. + * + * @var string + */ + public $charset = 'UTF-8'; + + /** + * XSS Hash + * + * Random Hash for protecting URLs. + * + * @var string + */ + protected $_xss_hash; + + /** + * CSRF Hash + * + * Random hash for Cross Site Request Forgery protection cookie + * + * @var string + */ + protected $_csrf_hash; + + /** + * CSRF Expire time + * + * Expiration time for Cross Site Request Forgery protection cookie. + * Defaults to two hours (in seconds). + * + * @var int + */ + protected $_csrf_expire = 7200; + + /** + * CSRF Token name + * + * Token name for Cross Site Request Forgery protection cookie. + * + * @var string + */ + protected $_csrf_token_name = 'ci_csrf_token'; + + /** + * CSRF Cookie name + * + * Cookie name for Cross Site Request Forgery protection cookie. + * + * @var string + */ + protected $_csrf_cookie_name = 'ci_csrf_token'; + + /** + * List of never allowed strings + * + * @var array + */ + protected $_never_allowed_str = array( + 'document.cookie' => '[removed]', + 'document.write' => '[removed]', + '.parentNode' => '[removed]', + '.innerHTML' => '[removed]', + '-moz-binding' => '[removed]', + '' => '-->', + ' '<![CDATA[', + '' => '<comment>', + '<%' => '<%' + ); + + /** + * List of never allowed regex replacements + * + * @var array + */ + protected $_never_allowed_regex = array( + 'javascript\s*:', + '(document|(document\.)?window)\.(location|on\w*)', + 'expression\s*(\(|&\#40;)', // CSS and IE + 'vbscript\s*:', // IE, surprise! + 'wscript\s*:', // IE + 'jscript\s*:', // IE + 'vbs\s*:', // IE + 'Redirect\s+30\d', + "([\"'])?data\s*:[^\\1]*?base64[^\\1]*?,[^\\1]*?\\1?" + ); + + /** + * Class constructor + * + * @return void + */ + public function __construct() + { + // Is CSRF protection enabled? + if (config_item('csrf_protection')) + { + // CSRF config + foreach (array('csrf_expire', 'csrf_token_name', 'csrf_cookie_name') as $key) + { + if (NULL !== ($val = config_item($key))) + { + $this->{'_'.$key} = $val; + } + } + + // Append application specific cookie prefix + if ($cookie_prefix = config_item('cookie_prefix')) + { + $this->_csrf_cookie_name = $cookie_prefix.$this->_csrf_cookie_name; + } + + // Set the CSRF hash + $this->_csrf_set_hash(); + } + + $this->charset = strtoupper(config_item('charset')); + + log_message('info', 'Security Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * CSRF Verify + * + * @return CI_Security + */ + public function csrf_verify() + { + // If it's not a POST request we will set the CSRF cookie + if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST') + { + return $this->csrf_set_cookie(); + } + + // Check if URI has been whitelisted from CSRF checks + if ($exclude_uris = config_item('csrf_exclude_uris')) + { + $uri = load_class('URI', 'core'); + foreach ($exclude_uris as $excluded) + { + if (preg_match('#^'.$excluded.'$#i'.(UTF8_ENABLED ? 'u' : ''), $uri->uri_string())) + { + return $this; + } + } + } + + // Do the tokens exist in both the _POST and _COOKIE arrays? + if ( ! isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name]) + OR $_POST[$this->_csrf_token_name] !== $_COOKIE[$this->_csrf_cookie_name]) // Do the tokens match? + { + $this->csrf_show_error(); + } + + // We kill this since we're done and we don't want to pollute the _POST array + unset($_POST[$this->_csrf_token_name]); + + // Regenerate on every submission? + if (config_item('csrf_regenerate')) + { + // Nothing should last forever + unset($_COOKIE[$this->_csrf_cookie_name]); + $this->_csrf_hash = NULL; + } + + $this->_csrf_set_hash(); + $this->csrf_set_cookie(); + + log_message('info', 'CSRF token verified'); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * CSRF Set Cookie + * + * @codeCoverageIgnore + * @return CI_Security + */ + public function csrf_set_cookie() + { + $expire = time() + $this->_csrf_expire; + $secure_cookie = (bool) config_item('cookie_secure'); + + if ($secure_cookie && ! is_https()) + { + return FALSE; + } + + setcookie( + $this->_csrf_cookie_name, + $this->_csrf_hash, + $expire, + config_item('cookie_path'), + config_item('cookie_domain'), + $secure_cookie, + config_item('cookie_httponly') + ); + log_message('info', 'CSRF cookie sent'); + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Show CSRF Error + * + * @return void + */ + public function csrf_show_error() + { + show_error('The action you have requested is not allowed.', 403); + } + + // -------------------------------------------------------------------- + + /** + * Get CSRF Hash + * + * @see CI_Security::$_csrf_hash + * @return string CSRF hash + */ + public function get_csrf_hash() + { + return $this->_csrf_hash; + } + + // -------------------------------------------------------------------- + + /** + * Get CSRF Token Name + * + * @see CI_Security::$_csrf_token_name + * @return string CSRF token name + */ + public function get_csrf_token_name() + { + return $this->_csrf_token_name; + } + + // -------------------------------------------------------------------- + + /** + * XSS Clean + * + * Sanitizes data so that Cross Site Scripting Hacks can be + * prevented. This method does a fair amount of work but + * it is extremely thorough, designed to prevent even the + * most obscure XSS attempts. Nothing is ever 100% foolproof, + * of course, but I haven't been able to get anything passed + * the filter. + * + * Note: Should only be used to deal with data upon submission. + * It's not something that should be used for general + * runtime processing. + * + * @link http://channel.bitflux.ch/wiki/XSS_Prevention + * Based in part on some code and ideas from Bitflux. + * + * @link http://ha.ckers.org/xss.html + * To help develop this script I used this great list of + * vulnerabilities along with a few other hacks I've + * harvested from examining vulnerabilities in other programs. + * + * @param string|string[] $str Input data + * @param bool $is_image Whether the input is an image + * @return string + */ + public function xss_clean($str, $is_image = FALSE) + { + // Is the string an array? + if (is_array($str)) + { + while (list($key) = each($str)) + { + $str[$key] = $this->xss_clean($str[$key]); + } + + return $str; + } + + // Remove Invisible Characters + $str = remove_invisible_characters($str); + + /* + * URL Decode + * + * Just in case stuff like this is submitted: + * + * Google + * + * Note: Use rawurldecode() so it does not remove plus signs + */ + if (stripos($str, '%') !== false) + { + do + { + $oldstr = $str; + $str = rawurldecode($str); + $str = preg_replace_callback('#%(?:\s*[0-9a-f]){2,}#i', array($this, '_urldecodespaces'), $str); + } + while ($oldstr !== $str); + unset($oldstr); + } + + /* + * Convert character entities to ASCII + * + * This permits our tests below to work reliably. + * We only convert entities that are within tags since + * these are the ones that will pose security problems. + */ + $str = preg_replace_callback("/[^a-z0-9>]+[a-z0-9]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str); + $str = preg_replace_callback('/<\w+.*/si', array($this, '_decode_entity'), $str); + + // Remove Invisible Characters Again! + $str = remove_invisible_characters($str); + + /* + * Convert all tabs to spaces + * + * This prevents strings like this: ja vascript + * NOTE: we deal with spaces between characters later. + * NOTE: preg_replace was found to be amazingly slow here on + * large blocks of data, so we use str_replace. + */ + $str = str_replace("\t", ' ', $str); + + // Capture converted string for later comparison + $converted_string = $str; + + // Remove Strings that are never allowed + $str = $this->_do_never_allowed($str); + + /* + * Makes PHP tags safe + * + * Note: XML tags are inadvertently replaced too: + * + * '), array('<?', '?>'), $str); + } + + /* + * Compact any exploded words + * + * This corrects words like: j a v a s c r i p t + * These words are compacted back to their correct state. + */ + $words = array( + 'javascript', 'expression', 'vbscript', 'jscript', 'wscript', + 'vbs', 'script', 'base64', 'applet', 'alert', 'document', + 'write', 'cookie', 'window', 'confirm', 'prompt', 'eval' + ); + + foreach ($words as $word) + { + $word = implode('\s*', str_split($word)).'\s*'; + + // We only want to do this when it is followed by a non-word character + // That way valid stuff like "dealer to" does not become "dealerto" + $str = preg_replace_callback('#('.substr($word, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str); + } + + /* + * Remove disallowed Javascript in links or img tags + * We used to do some version comparisons and use of stripos(), + * but it is dog slow compared to these simplified non-capturing + * preg_match(), especially if the pattern exists in the string + * + * Note: It was reported that not only space characters, but all in + * the following pattern can be parsed as separators between a tag name + * and its attributes: [\d\s"\'`;,\/\=\(\x00\x0B\x09\x0C] + * ... however, remove_invisible_characters() above already strips the + * hex-encoded ones, so we'll skip them below. + */ + do + { + $original = $str; + + if (preg_match('/]+([^>]*?)(?:>|$)#si', array($this, '_js_link_removal'), $str); + } + + if (preg_match('/]*?)(?:\s?/?>|$)#si', array($this, '_js_img_removal'), $str); + } + + if (preg_match('/script|xss/i', $str)) + { + $str = preg_replace('##si', '[removed]', $str); + } + } + while ($original !== $str); + unset($original); + + /* + * Sanitize naughty HTML elements + * + * If a tag containing any of the words in the list + * below is found, the tag gets converted to entities. + * + * So this: + * Becomes: <blink> + */ + $pattern = '#' + .'<((?/*\s*)(?[a-z0-9]+)(?=[^a-z0-9]|$)' // tag start and name, followed by a non-tag character + .'[^\s\042\047a-z0-9>/=]*' // a valid attribute character immediately after the tag would count as a separator + // optional attributes + .'(?(?:[\s\042\047/=]*' // non-attribute characters, excluding > (tag close) for obvious reasons + .'[^\s\042\047>/=]+' // attribute characters + // optional attribute-value + .'(?:\s*=' // attribute-value separator + .'(?:[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*))' // single, double or non-quoted value + .')?' // end optional attribute-value group + .')*)' // end optional attributes group + .'[^>]*)(?\>)?#isS'; + + // Note: It would be nice to optimize this for speed, BUT + // only matching the naughty elements here results in + // false positives and in turn - vulnerabilities! + do + { + $old_str = $str; + $str = preg_replace_callback($pattern, array($this, '_sanitize_naughty_html'), $str); + } + while ($old_str !== $str); + unset($old_str); + + /* + * Sanitize naughty scripting elements + * + * Similar to above, only instead of looking for + * tags it looks for PHP and JavaScript commands + * that are disallowed. Rather than removing the + * code, it simply converts the parenthesis to entities + * rendering the code un-executable. + * + * For example: eval('some code') + * Becomes: eval('some code') + */ + $str = preg_replace( + '#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si', + '\\1\\2(\\3)', + $str + ); + + // Final clean up + // This adds a bit of extra precaution in case + // something got through the above filters + $str = $this->_do_never_allowed($str); + + /* + * Images are Handled in a Special Way + * - Essentially, we want to know that after all of the character + * conversion is done whether any unwanted, likely XSS, code was found. + * If not, we return TRUE, as the image is clean. + * However, if the string post-conversion does not matched the + * string post-removal of XSS, then it fails, as there was unwanted XSS + * code found and removed/changed during processing. + */ + if ($is_image === TRUE) + { + return ($str === $converted_string); + } + + return $str; + } + + // -------------------------------------------------------------------- + + /** + * XSS Hash + * + * Generates the XSS hash if needed and returns it. + * + * @see CI_Security::$_xss_hash + * @return string XSS hash + */ + public function xss_hash() + { + if ($this->_xss_hash === NULL) + { + $rand = $this->get_random_bytes(16); + $this->_xss_hash = ($rand === FALSE) + ? md5(uniqid(mt_rand(), TRUE)) + : bin2hex($rand); + } + + return $this->_xss_hash; + } + + // -------------------------------------------------------------------- + + /** + * Get random bytes + * + * @param int $length Output length + * @return string + */ + public function get_random_bytes($length) + { + if (empty($length) OR ! ctype_digit((string) $length)) + { + return FALSE; + } + + if (function_exists('random_bytes')) + { + try + { + // The cast is required to avoid TypeError + return random_bytes((int) $length); + } + catch (Exception $e) + { + // If random_bytes() can't do the job, we can't either ... + // There's no point in using fallbacks. + log_message('error', $e->getMessage()); + return FALSE; + } + } + + // Unfortunately, none of the following PRNGs is guaranteed to exist ... + if (defined('MCRYPT_DEV_URANDOM') && ($output = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM)) !== FALSE) + { + return $output; + } + + + if (is_readable('/dev/urandom') && ($fp = fopen('/dev/urandom', 'rb')) !== FALSE) + { + // Try not to waste entropy ... + is_php('5.4') && stream_set_chunk_size($fp, $length); + $output = fread($fp, $length); + fclose($fp); + if ($output !== FALSE) + { + return $output; + } + } + + if (function_exists('openssl_random_pseudo_bytes')) + { + return openssl_random_pseudo_bytes($length); + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * HTML Entities Decode + * + * A replacement for html_entity_decode() + * + * The reason we are not using html_entity_decode() by itself is because + * while it is not technically correct to leave out the semicolon + * at the end of an entity most browsers will still interpret the entity + * correctly. html_entity_decode() does not convert entities without + * semicolons, so we are left with our own little solution here. Bummer. + * + * @link http://php.net/html-entity-decode + * + * @param string $str Input + * @param string $charset Character set + * @return string + */ + public function entity_decode($str, $charset = NULL) + { + if (strpos($str, '&') === FALSE) + { + return $str; + } + + static $_entities; + + isset($charset) OR $charset = $this->charset; + $flag = is_php('5.4') + ? ENT_COMPAT | ENT_HTML5 + : ENT_COMPAT; + + if ( ! isset($_entities)) + { + $_entities = array_map('strtolower', get_html_translation_table(HTML_ENTITIES, $flag, $charset)); + + // If we're not on PHP 5.4+, add the possibly dangerous HTML 5 + // entities to the array manually + if ($flag === ENT_COMPAT) + { + $_entities[':'] = ':'; + $_entities['('] = '('; + $_entities[')'] = ')'; + $_entities["\n"] = ' '; + $_entities["\t"] = ' '; + } + } + + do + { + $str_compare = $str; + + // Decode standard entities, avoiding false positives + if (preg_match_all('/&[a-z]{2,}(?![a-z;])/i', $str, $matches)) + { + $replace = array(); + $matches = array_unique(array_map('strtolower', $matches[0])); + foreach ($matches as &$match) + { + if (($char = array_search($match.';', $_entities, TRUE)) !== FALSE) + { + $replace[$match] = $char; + } + } + + $str = str_replace(array_keys($replace), array_values($replace), $str); + } + + // Decode numeric & UTF16 two byte entities + $str = html_entity_decode( + preg_replace('/(&#(?:x0*[0-9a-f]{2,5}(?![0-9a-f;])|(?:0*\d{2,4}(?![0-9;]))))/iS', '$1;', $str), + $flag, + $charset + ); + + if ($flag === ENT_COMPAT) + { + $str = str_replace(array_values($_entities), array_keys($_entities), $str); + } + } + while ($str_compare !== $str); + return $str; + } + + // -------------------------------------------------------------------- + + /** + * Sanitize Filename + * + * @param string $str Input file name + * @param bool $relative_path Whether to preserve paths + * @return string + */ + public function sanitize_filename($str, $relative_path = FALSE) + { + $bad = $this->filename_bad_chars; + + if ( ! $relative_path) + { + $bad[] = './'; + $bad[] = '/'; + } + + $str = remove_invisible_characters($str, FALSE); + + do + { + $old = $str; + $str = str_replace($bad, '', $str); + } + while ($old !== $str); + + return stripslashes($str); + } + + // ---------------------------------------------------------------- + + /** + * Strip Image Tags + * + * @param string $str + * @return string + */ + public function strip_image_tags($str) + { + return preg_replace( + array( + '##i', + '#`]+)).*?\>#i' + ), + '\\2', + $str + ); + } + + // ---------------------------------------------------------------- + + /** + * URL-decode taking spaces into account + * + * @see https://github.com/bcit-ci/CodeIgniter/issues/4877 + * @param array $matches + * @return string + */ + protected function _urldecodespaces($matches) + { + $input = $matches[0]; + $nospaces = preg_replace('#\s+#', '', $input); + return ($nospaces === $input) + ? $input + : rawurldecode($nospaces); + } + + // ---------------------------------------------------------------- + + /** + * Compact Exploded Words + * + * Callback method for xss_clean() to remove whitespace from + * things like 'j a v a s c r i p t'. + * + * @used-by CI_Security::xss_clean() + * @param array $matches + * @return string + */ + protected function _compact_exploded_words($matches) + { + return preg_replace('/\s+/s', '', $matches[1]).$matches[2]; + } + + // -------------------------------------------------------------------- + + /** + * Sanitize Naughty HTML + * + * Callback method for xss_clean() to remove naughty HTML elements. + * + * @used-by CI_Security::xss_clean() + * @param array $matches + * @return string + */ + protected function _sanitize_naughty_html($matches) + { + static $naughty_tags = array( + 'alert', 'area', 'prompt', 'confirm', 'applet', 'audio', 'basefont', 'base', 'behavior', 'bgsound', + 'blink', 'body', 'embed', 'expression', 'form', 'frameset', 'frame', 'head', 'html', 'ilayer', + 'iframe', 'input', 'button', 'select', 'isindex', 'layer', 'link', 'meta', 'keygen', 'object', + 'plaintext', 'style', 'script', 'textarea', 'title', 'math', 'video', 'svg', 'xml', 'xss' + ); + + static $evil_attributes = array( + 'on\w+', 'style', 'xmlns', 'formaction', 'form', 'xlink:href', 'FSCommand', 'seekSegmentTime' + ); + + // First, escape unclosed tags + if (empty($matches['closeTag'])) + { + return '<'.$matches[1]; + } + // Is the element that we caught naughty? If so, escape it + elseif (in_array(strtolower($matches['tagName']), $naughty_tags, TRUE)) + { + return '<'.$matches[1].'>'; + } + // For other tags, see if their attributes are "evil" and strip those + elseif (isset($matches['attributes'])) + { + // We'll store the already fitlered attributes here + $attributes = array(); + + // Attribute-catching pattern + $attributes_pattern = '#' + .'(?[^\s\042\047>/=]+)' // attribute characters + // optional attribute-value + .'(?:\s*=(?[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*)))' // attribute-value separator + .'#i'; + + // Blacklist pattern for evil attribute names + $is_evil_pattern = '#^('.implode('|', $evil_attributes).')$#i'; + + // Each iteration filters a single attribute + do + { + // Strip any non-alpha characters that may preceed an attribute. + // Browsers often parse these incorrectly and that has been a + // of numerous XSS issues we've had. + $matches['attributes'] = preg_replace('#^[^a-z]+#i', '', $matches['attributes']); + + if ( ! preg_match($attributes_pattern, $matches['attributes'], $attribute, PREG_OFFSET_CAPTURE)) + { + // No (valid) attribute found? Discard everything else inside the tag + break; + } + + if ( + // Is it indeed an "evil" attribute? + preg_match($is_evil_pattern, $attribute['name'][0]) + // Or does it have an equals sign, but no value and not quoted? Strip that too! + OR (trim($attribute['value'][0]) === '') + ) + { + $attributes[] = 'xss=removed'; + } + else + { + $attributes[] = $attribute[0][0]; + } + + $matches['attributes'] = substr($matches['attributes'], $attribute[0][1] + strlen($attribute[0][0])); + } + while ($matches['attributes'] !== ''); + + $attributes = empty($attributes) + ? '' + : ' '.implode(' ', $attributes); + return '<'.$matches['slash'].$matches['tagName'].$attributes.'>'; + } + + return $matches[0]; + } + + // -------------------------------------------------------------------- + + /** + * JS Link Removal + * + * Callback method for xss_clean() to sanitize links. + * + * This limits the PCRE backtracks, making it more performance friendly + * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in + * PHP 5.2+ on link-heavy strings. + * + * @used-by CI_Security::xss_clean() + * @param array $match + * @return string + */ + protected function _js_link_removal($match) + { + return str_replace( + $match[1], + preg_replace( + '#href=.*?(?:(?:alert|prompt|confirm)(?:\(|&\#40;)|javascript:|livescript:|mocha:|charset=|window\.|document\.|\.cookie|_filter_attributes($match[1]) + ), + $match[0] + ); + } + + // -------------------------------------------------------------------- + + /** + * JS Image Removal + * + * Callback method for xss_clean() to sanitize image tags. + * + * This limits the PCRE backtracks, making it more performance friendly + * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in + * PHP 5.2+ on image tag heavy strings. + * + * @used-by CI_Security::xss_clean() + * @param array $match + * @return string + */ + protected function _js_img_removal($match) + { + return str_replace( + $match[1], + preg_replace( + '#src=.*?(?:(?:alert|prompt|confirm|eval)(?:\(|&\#40;)|javascript:|livescript:|mocha:|charset=|window\.|document\.|\.cookie|_filter_attributes($match[1]) + ), + $match[0] + ); + } + + // -------------------------------------------------------------------- + + /** + * Attribute Conversion + * + * @used-by CI_Security::xss_clean() + * @param array $match + * @return string + */ + protected function _convert_attribute($match) + { + return str_replace(array('>', '<', '\\'), array('>', '<', '\\\\'), $match[0]); + } + + // -------------------------------------------------------------------- + + /** + * Filter Attributes + * + * Filters tag attributes for consistency and safety. + * + * @used-by CI_Security::_js_img_removal() + * @used-by CI_Security::_js_link_removal() + * @param string $str + * @return string + */ + protected function _filter_attributes($str) + { + $out = ''; + if (preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches)) + { + foreach ($matches[0] as $match) + { + $out .= preg_replace('#/\*.*?\*/#s', '', $match); + } + } + + return $out; + } + + // -------------------------------------------------------------------- + + /** + * HTML Entity Decode Callback + * + * @used-by CI_Security::xss_clean() + * @param array $match + * @return string + */ + protected function _decode_entity($match) + { + // Protect GET variables in URLs + // 901119URL5918AMP18930PROTECT8198 + $match = preg_replace('|\&([a-z\_0-9\-]+)\=([a-z\_0-9\-/]+)|i', $this->xss_hash().'\\1=\\2', $match[0]); + + // Decode, then un-protect URL GET vars + return str_replace( + $this->xss_hash(), + '&', + $this->entity_decode($match, $this->charset) + ); + } + + // -------------------------------------------------------------------- + + /** + * Do Never Allowed + * + * @used-by CI_Security::xss_clean() + * @param string + * @return string + */ + protected function _do_never_allowed($str) + { + $str = str_replace(array_keys($this->_never_allowed_str), $this->_never_allowed_str, $str); + + foreach ($this->_never_allowed_regex as $regex) + { + $str = preg_replace('#'.$regex.'#is', '[removed]', $str); + } + + return $str; + } + + // -------------------------------------------------------------------- + + /** + * Set CSRF Hash and Cookie + * + * @return string + */ + protected function _csrf_set_hash() + { + if ($this->_csrf_hash === NULL) + { + // If the cookie exists we will use its value. + // We don't necessarily want to regenerate it with + // each page load since a page could contain embedded + // sub-pages causing this feature to fail + if (isset($_COOKIE[$this->_csrf_cookie_name]) && is_string($_COOKIE[$this->_csrf_cookie_name]) + && preg_match('#^[0-9a-f]{32}$#iS', $_COOKIE[$this->_csrf_cookie_name]) === 1) + { + return $this->_csrf_hash = $_COOKIE[$this->_csrf_cookie_name]; + } + + $rand = $this->get_random_bytes(16); + $this->_csrf_hash = ($rand === FALSE) + ? md5(uniqid(mt_rand(), TRUE)) + : bin2hex($rand); + } + + return $this->_csrf_hash; + } + +} diff --git a/system/core/URI.php b/system/core/URI.php new file mode 100644 index 0000000..544f6c8 --- /dev/null +++ b/system/core/URI.php @@ -0,0 +1,643 @@ +config =& load_class('Config', 'core'); + + // If query strings are enabled, we don't need to parse any segments. + // However, they don't make sense under CLI. + if (is_cli() OR $this->config->item('enable_query_strings') !== TRUE) + { + $this->_permitted_uri_chars = $this->config->item('permitted_uri_chars'); + + // If it's a CLI request, ignore the configuration + if (is_cli()) + { + $uri = $this->_parse_argv(); + } + else + { + $protocol = $this->config->item('uri_protocol'); + empty($protocol) && $protocol = 'REQUEST_URI'; + + switch ($protocol) + { + case 'AUTO': // For BC purposes only + case 'REQUEST_URI': + $uri = $this->_parse_request_uri(); + break; + case 'QUERY_STRING': + $uri = $this->_parse_query_string(); + break; + case 'PATH_INFO': + default: + $uri = isset($_SERVER[$protocol]) + ? $_SERVER[$protocol] + : $this->_parse_request_uri(); + break; + } + } + + $this->_set_uri_string($uri); + } + + log_message('info', 'URI Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Set URI String + * + * @param string $str + * @return void + */ + protected function _set_uri_string($str) + { + // Filter out control characters and trim slashes + $this->uri_string = trim(remove_invisible_characters($str, FALSE), '/'); + + if ($this->uri_string !== '') + { + // Remove the URL suffix, if present + if (($suffix = (string) $this->config->item('url_suffix')) !== '') + { + $slen = strlen($suffix); + + if (substr($this->uri_string, -$slen) === $suffix) + { + $this->uri_string = substr($this->uri_string, 0, -$slen); + } + } + + $this->segments[0] = NULL; + // Populate the segments array + foreach (explode('/', trim($this->uri_string, '/')) as $val) + { + $val = trim($val); + // Filter segments for security + $this->filter_uri($val); + + if ($val !== '') + { + $this->segments[] = $val; + } + } + + unset($this->segments[0]); + } + } + + // -------------------------------------------------------------------- + + /** + * Parse REQUEST_URI + * + * Will parse REQUEST_URI and automatically detect the URI from it, + * while fixing the query string if necessary. + * + * @return string + */ + protected function _parse_request_uri() + { + if ( ! isset($_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME'])) + { + return ''; + } + + // parse_url() returns false if no host is present, but the path or query string + // contains a colon followed by a number + $uri = parse_url('http://dummy'.$_SERVER['REQUEST_URI']); + $query = isset($uri['query']) ? $uri['query'] : ''; + $uri = isset($uri['path']) ? $uri['path'] : ''; + + if (isset($_SERVER['SCRIPT_NAME'][0])) + { + if (strpos($uri, $_SERVER['SCRIPT_NAME']) === 0) + { + $uri = (string) substr($uri, strlen($_SERVER['SCRIPT_NAME'])); + } + elseif (strpos($uri, dirname($_SERVER['SCRIPT_NAME'])) === 0) + { + $uri = (string) substr($uri, strlen(dirname($_SERVER['SCRIPT_NAME']))); + } + } + + // This section ensures that even on servers that require the URI to be in the query string (Nginx) a correct + // URI is found, and also fixes the QUERY_STRING server var and $_GET array. + if (trim($uri, '/') === '' && strncmp($query, '/', 1) === 0) + { + $query = explode('?', $query, 2); + $uri = $query[0]; + $_SERVER['QUERY_STRING'] = isset($query[1]) ? $query[1] : ''; + } + else + { + $_SERVER['QUERY_STRING'] = $query; + } + + parse_str($_SERVER['QUERY_STRING'], $_GET); + + if ($uri === '/' OR $uri === '') + { + return '/'; + } + + // Do some final cleaning of the URI and return it + return $this->_remove_relative_directory($uri); + } + + // -------------------------------------------------------------------- + + /** + * Parse QUERY_STRING + * + * Will parse QUERY_STRING and automatically detect the URI from it. + * + * @return string + */ + protected function _parse_query_string() + { + $uri = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : @getenv('QUERY_STRING'); + + if (trim($uri, '/') === '') + { + return ''; + } + elseif (strncmp($uri, '/', 1) === 0) + { + $uri = explode('?', $uri, 2); + $_SERVER['QUERY_STRING'] = isset($uri[1]) ? $uri[1] : ''; + $uri = $uri[0]; + } + + parse_str($_SERVER['QUERY_STRING'], $_GET); + + return $this->_remove_relative_directory($uri); + } + + // -------------------------------------------------------------------- + + /** + * Parse CLI arguments + * + * Take each command line argument and assume it is a URI segment. + * + * @return string + */ + protected function _parse_argv() + { + $args = array_slice($_SERVER['argv'], 1); + return $args ? implode('/', $args) : ''; + } + + // -------------------------------------------------------------------- + + /** + * Remove relative directory (../) and multi slashes (///) + * + * Do some final cleaning of the URI and return it, currently only used in self::_parse_request_uri() + * + * @param string $uri + * @return string + */ + protected function _remove_relative_directory($uri) + { + $uris = array(); + $tok = strtok($uri, '/'); + while ($tok !== FALSE) + { + if (( ! empty($tok) OR $tok === '0') && $tok !== '..') + { + $uris[] = $tok; + } + $tok = strtok('/'); + } + + return implode('/', $uris); + } + + // -------------------------------------------------------------------- + + /** + * Filter URI + * + * Filters segments for malicious characters. + * + * @param string $str + * @return void + */ + public function filter_uri(&$str) + { + if ( ! empty($str) && ! empty($this->_permitted_uri_chars) && ! preg_match('/^['.$this->_permitted_uri_chars.']+$/i'.(UTF8_ENABLED ? 'u' : ''), $str)) + { + show_error('The URI you submitted has disallowed characters.', 400); + } + } + + // -------------------------------------------------------------------- + + /** + * Fetch URI Segment + * + * @see CI_URI::$segments + * @param int $n Index + * @param mixed $no_result What to return if the segment index is not found + * @return mixed + */ + public function segment($n, $no_result = NULL) + { + return isset($this->segments[$n]) ? $this->segments[$n] : $no_result; + } + + // -------------------------------------------------------------------- + + /** + * Fetch URI "routed" Segment + * + * Returns the re-routed URI segment (assuming routing rules are used) + * based on the index provided. If there is no routing, will return + * the same result as CI_URI::segment(). + * + * @see CI_URI::$rsegments + * @see CI_URI::segment() + * @param int $n Index + * @param mixed $no_result What to return if the segment index is not found + * @return mixed + */ + public function rsegment($n, $no_result = NULL) + { + return isset($this->rsegments[$n]) ? $this->rsegments[$n] : $no_result; + } + + // -------------------------------------------------------------------- + + /** + * URI to assoc + * + * Generates an associative array of URI data starting at the supplied + * segment index. For example, if this is your URI: + * + * example.com/user/search/name/joe/location/UK/gender/male + * + * You can use this method to generate an array with this prototype: + * + * array ( + * name => joe + * location => UK + * gender => male + * ) + * + * @param int $n Index (default: 3) + * @param array $default Default values + * @return array + */ + public function uri_to_assoc($n = 3, $default = array()) + { + return $this->_uri_to_assoc($n, $default, 'segment'); + } + + // -------------------------------------------------------------------- + + /** + * Routed URI to assoc + * + * Identical to CI_URI::uri_to_assoc(), only it uses the re-routed + * segment array. + * + * @see CI_URI::uri_to_assoc() + * @param int $n Index (default: 3) + * @param array $default Default values + * @return array + */ + public function ruri_to_assoc($n = 3, $default = array()) + { + return $this->_uri_to_assoc($n, $default, 'rsegment'); + } + + // -------------------------------------------------------------------- + + /** + * Internal URI-to-assoc + * + * Generates a key/value pair from the URI string or re-routed URI string. + * + * @used-by CI_URI::uri_to_assoc() + * @used-by CI_URI::ruri_to_assoc() + * @param int $n Index (default: 3) + * @param array $default Default values + * @param string $which Array name ('segment' or 'rsegment') + * @return array + */ + protected function _uri_to_assoc($n = 3, $default = array(), $which = 'segment') + { + if ( ! is_numeric($n)) + { + return $default; + } + + if (isset($this->keyval[$which], $this->keyval[$which][$n])) + { + return $this->keyval[$which][$n]; + } + + $total_segments = "total_{$which}s"; + $segment_array = "{$which}_array"; + + if ($this->$total_segments() < $n) + { + return (count($default) === 0) + ? array() + : array_fill_keys($default, NULL); + } + + $segments = array_slice($this->$segment_array(), ($n - 1)); + $i = 0; + $lastval = ''; + $retval = array(); + foreach ($segments as $seg) + { + if ($i % 2) + { + $retval[$lastval] = $seg; + } + else + { + $retval[$seg] = NULL; + $lastval = $seg; + } + + $i++; + } + + if (count($default) > 0) + { + foreach ($default as $val) + { + if ( ! array_key_exists($val, $retval)) + { + $retval[$val] = NULL; + } + } + } + + // Cache the array for reuse + isset($this->keyval[$which]) OR $this->keyval[$which] = array(); + $this->keyval[$which][$n] = $retval; + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Assoc to URI + * + * Generates a URI string from an associative array. + * + * @param array $array Input array of key/value pairs + * @return string URI string + */ + public function assoc_to_uri($array) + { + $temp = array(); + foreach ((array) $array as $key => $val) + { + $temp[] = $key; + $temp[] = $val; + } + + return implode('/', $temp); + } + + // -------------------------------------------------------------------- + + /** + * Slash segment + * + * Fetches an URI segment with a slash. + * + * @param int $n Index + * @param string $where Where to add the slash ('trailing' or 'leading') + * @return string + */ + public function slash_segment($n, $where = 'trailing') + { + return $this->_slash_segment($n, $where, 'segment'); + } + + // -------------------------------------------------------------------- + + /** + * Slash routed segment + * + * Fetches an URI routed segment with a slash. + * + * @param int $n Index + * @param string $where Where to add the slash ('trailing' or 'leading') + * @return string + */ + public function slash_rsegment($n, $where = 'trailing') + { + return $this->_slash_segment($n, $where, 'rsegment'); + } + + // -------------------------------------------------------------------- + + /** + * Internal Slash segment + * + * Fetches an URI Segment and adds a slash to it. + * + * @used-by CI_URI::slash_segment() + * @used-by CI_URI::slash_rsegment() + * + * @param int $n Index + * @param string $where Where to add the slash ('trailing' or 'leading') + * @param string $which Array name ('segment' or 'rsegment') + * @return string + */ + protected function _slash_segment($n, $where = 'trailing', $which = 'segment') + { + $leading = $trailing = '/'; + + if ($where === 'trailing') + { + $leading = ''; + } + elseif ($where === 'leading') + { + $trailing = ''; + } + + return $leading.$this->$which($n).$trailing; + } + + // -------------------------------------------------------------------- + + /** + * Segment Array + * + * @return array CI_URI::$segments + */ + public function segment_array() + { + return $this->segments; + } + + // -------------------------------------------------------------------- + + /** + * Routed Segment Array + * + * @return array CI_URI::$rsegments + */ + public function rsegment_array() + { + return $this->rsegments; + } + + // -------------------------------------------------------------------- + + /** + * Total number of segments + * + * @return int + */ + public function total_segments() + { + return count($this->segments); + } + + // -------------------------------------------------------------------- + + /** + * Total number of routed segments + * + * @return int + */ + public function total_rsegments() + { + return count($this->rsegments); + } + + // -------------------------------------------------------------------- + + /** + * Fetch URI string + * + * @return string CI_URI::$uri_string + */ + public function uri_string() + { + return $this->uri_string; + } + + // -------------------------------------------------------------------- + + /** + * Fetch Re-routed URI string + * + * @return string + */ + public function ruri_string() + { + return ltrim(load_class('Router', 'core')->directory, '/').implode('/', $this->rsegments); + } + +} diff --git a/system/core/Utf8.php b/system/core/Utf8.php new file mode 100644 index 0000000..f2f42e6 --- /dev/null +++ b/system/core/Utf8.php @@ -0,0 +1,164 @@ +is_ascii($str) === FALSE) + { + if (MB_ENABLED) + { + $str = mb_convert_encoding($str, 'UTF-8', 'UTF-8'); + } + elseif (ICONV_ENABLED) + { + $str = @iconv('UTF-8', 'UTF-8//IGNORE', $str); + } + } + + return $str; + } + + // -------------------------------------------------------------------- + + /** + * Remove ASCII control characters + * + * Removes all ASCII control characters except horizontal tabs, + * line feeds, and carriage returns, as all others can cause + * problems in XML. + * + * @param string $str String to clean + * @return string + */ + public function safe_ascii_for_xml($str) + { + return remove_invisible_characters($str, FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Convert to UTF-8 + * + * Attempts to convert a string to UTF-8. + * + * @param string $str Input string + * @param string $encoding Input encoding + * @return string $str encoded in UTF-8 or FALSE on failure + */ + public function convert_to_utf8($str, $encoding) + { + if (MB_ENABLED) + { + return mb_convert_encoding($str, 'UTF-8', $encoding); + } + elseif (ICONV_ENABLED) + { + return @iconv($encoding, 'UTF-8', $str); + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Is ASCII? + * + * Tests if a string is standard 7-bit ASCII or not. + * + * @param string $str String to check + * @return bool + */ + public function is_ascii($str) + { + return (preg_match('/[^\x00-\x7F]/S', $str) === 0); + } + +} diff --git a/system/core/compat/hash.php b/system/core/compat/hash.php new file mode 100644 index 0000000..d567d0f --- /dev/null +++ b/system/core/compat/hash.php @@ -0,0 +1,245 @@ + 32, + 'haval128,3' => 128, + 'haval160,3' => 128, + 'haval192,3' => 128, + 'haval224,3' => 128, + 'haval256,3' => 128, + 'haval128,4' => 128, + 'haval160,4' => 128, + 'haval192,4' => 128, + 'haval224,4' => 128, + 'haval256,4' => 128, + 'haval128,5' => 128, + 'haval160,5' => 128, + 'haval192,5' => 128, + 'haval224,5' => 128, + 'haval256,5' => 128, + 'md2' => 16, + 'md4' => 64, + 'md5' => 64, + 'ripemd128' => 64, + 'ripemd160' => 64, + 'ripemd256' => 64, + 'ripemd320' => 64, + 'salsa10' => 64, + 'salsa20' => 64, + 'sha1' => 64, + 'sha224' => 64, + 'sha256' => 64, + 'sha384' => 128, + 'sha512' => 128, + 'snefru' => 32, + 'snefru256' => 32, + 'tiger128,3' => 64, + 'tiger160,3' => 64, + 'tiger192,3' => 64, + 'tiger128,4' => 64, + 'tiger160,4' => 64, + 'tiger192,4' => 64, + 'whirlpool' => 64 + ); + + if (isset($block_sizes[$algo]) && strlen($password) > $block_sizes[$algo]) + { + $password = hash($algo, $password, TRUE); + } + + $hash = ''; + // Note: Blocks are NOT 0-indexed + for ($bc = ceil($length / $hash_length), $bi = 1; $bi <= $bc; $bi++) + { + $key = $derived_key = hash_hmac($algo, $salt.pack('N', $bi), $password, TRUE); + for ($i = 1; $i < $iterations; $i++) + { + $derived_key ^= $key = hash_hmac($algo, $key, $password, TRUE); + } + + $hash .= $derived_key; + } + + // This is not RFC-compatible, but we're aiming for natural PHP compatibility + return substr($raw_output ? $hash : bin2hex($hash), 0, $length); + } +} diff --git a/system/core/compat/index.html b/system/core/compat/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/core/compat/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/core/compat/mbstring.php b/system/core/compat/mbstring.php new file mode 100644 index 0000000..554d100 --- /dev/null +++ b/system/core/compat/mbstring.php @@ -0,0 +1,149 @@ + 0, 'algoName' => 'unknown', 'options' => array()) + : array('algo' => 1, 'algoName' => 'bcrypt', 'options' => array('cost' => $hash)); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('password_hash')) +{ + /** + * password_hash() + * + * @link http://php.net/password_hash + * @param string $password + * @param int $algo + * @param array $options + * @return mixed + */ + function password_hash($password, $algo, array $options = array()) + { + static $func_override; + isset($func_override) OR $func_override = (extension_loaded('mbstring') && ini_get('mbstring.func_override')); + + if ($algo !== 1) + { + trigger_error('password_hash(): Unknown hashing algorithm: '.(int) $algo, E_USER_WARNING); + return NULL; + } + + if (isset($options['cost']) && ($options['cost'] < 4 OR $options['cost'] > 31)) + { + trigger_error('password_hash(): Invalid bcrypt cost parameter specified: '.(int) $options['cost'], E_USER_WARNING); + return NULL; + } + + if (isset($options['salt']) && ($saltlen = ($func_override ? mb_strlen($options['salt'], '8bit') : strlen($options['salt']))) < 22) + { + trigger_error('password_hash(): Provided salt is too short: '.$saltlen.' expecting 22', E_USER_WARNING); + return NULL; + } + elseif ( ! isset($options['salt'])) + { + if (function_exists('random_bytes')) + { + try + { + $options['salt'] = random_bytes(16); + } + catch (Exception $e) + { + log_message('error', 'compat/password: Error while trying to use random_bytes(): '.$e->getMessage()); + return FALSE; + } + } + elseif (defined('MCRYPT_DEV_URANDOM')) + { + $options['salt'] = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM); + } + elseif (DIRECTORY_SEPARATOR === '/' && (is_readable($dev = '/dev/arandom') OR is_readable($dev = '/dev/urandom'))) + { + if (($fp = fopen($dev, 'rb')) === FALSE) + { + log_message('error', 'compat/password: Unable to open '.$dev.' for reading.'); + return FALSE; + } + + // Try not to waste entropy ... + is_php('5.4') && stream_set_chunk_size($fp, 16); + + $options['salt'] = ''; + for ($read = 0; $read < 16; $read = ($func_override) ? mb_strlen($options['salt'], '8bit') : strlen($options['salt'])) + { + if (($read = fread($fp, 16 - $read)) === FALSE) + { + log_message('error', 'compat/password: Error while reading from '.$dev.'.'); + return FALSE; + } + $options['salt'] .= $read; + } + + fclose($fp); + } + elseif (function_exists('openssl_random_pseudo_bytes')) + { + $is_secure = NULL; + $options['salt'] = openssl_random_pseudo_bytes(16, $is_secure); + if ($is_secure !== TRUE) + { + log_message('error', 'compat/password: openssl_random_pseudo_bytes() set the $cryto_strong flag to FALSE'); + return FALSE; + } + } + else + { + log_message('error', 'compat/password: No CSPRNG available.'); + return FALSE; + } + + $options['salt'] = str_replace('+', '.', rtrim(base64_encode($options['salt']), '=')); + } + elseif ( ! preg_match('#^[a-zA-Z0-9./]+$#D', $options['salt'])) + { + $options['salt'] = str_replace('+', '.', rtrim(base64_encode($options['salt']), '=')); + } + + isset($options['cost']) OR $options['cost'] = 10; + + return (strlen($password = crypt($password, sprintf('$2y$%02d$%s', $options['cost'], $options['salt']))) === 60) + ? $password + : FALSE; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('password_needs_rehash')) +{ + /** + * password_needs_rehash() + * + * @link http://php.net/password_needs_rehash + * @param string $hash + * @param int $algo + * @param array $options + * @return bool + */ + function password_needs_rehash($hash, $algo, array $options = array()) + { + $info = password_get_info($hash); + + if ($algo !== $info['algo']) + { + return TRUE; + } + elseif ($algo === 1) + { + $options['cost'] = isset($options['cost']) ? (int) $options['cost'] : 10; + return ($info['options']['cost'] !== $options['cost']); + } + + // Odd at first glance, but according to a comment in PHP's own unit tests, + // because it is an unknown algorithm - it's valid and therefore doesn't + // need rehashing. + return FALSE; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('password_verify')) +{ + /** + * password_verify() + * + * @link http://php.net/password_verify + * @param string $password + * @param string $hash + * @return bool + */ + function password_verify($password, $hash) + { + if (strlen($hash) !== 60 OR strlen($password = crypt($password, $hash)) !== 60) + { + return FALSE; + } + + $compare = 0; + for ($i = 0; $i < 60; $i++) + { + $compare |= (ord($password[$i]) ^ ord($hash[$i])); + } + + return ($compare === 0); + } +} diff --git a/system/core/compat/standard.php b/system/core/compat/standard.php new file mode 100644 index 0000000..6b7caa4 --- /dev/null +++ b/system/core/compat/standard.php @@ -0,0 +1,182 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/DB.php b/system/database/DB.php new file mode 100644 index 0000000..b4b7767 --- /dev/null +++ b/system/database/DB.php @@ -0,0 +1,218 @@ +load->get_package_paths() as $path) + { + if ($path !== APPPATH) + { + if (file_exists($file_path = $path.'config/'.ENVIRONMENT.'/database.php')) + { + include($file_path); + } + elseif (file_exists($file_path = $path.'config/database.php')) + { + include($file_path); + } + } + } + } + + if ( ! isset($db) OR count($db) === 0) + { + show_error('No database connection settings were found in the database config file.'); + } + + if ($params !== '') + { + $active_group = $params; + } + + if ( ! isset($active_group)) + { + show_error('You have not specified a database connection group via $active_group in your config/database.php file.'); + } + elseif ( ! isset($db[$active_group])) + { + show_error('You have specified an invalid database connection group ('.$active_group.') in your config/database.php file.'); + } + + $params = $db[$active_group]; + } + elseif (is_string($params)) + { + /** + * Parse the URL from the DSN string + * Database settings can be passed as discreet + * parameters or as a data source name in the first + * parameter. DSNs must have this prototype: + * $dsn = 'driver://username:password@hostname/database'; + */ + if (($dsn = @parse_url($params)) === FALSE) + { + show_error('Invalid DB Connection String'); + } + + $params = array( + 'dbdriver' => $dsn['scheme'], + 'hostname' => isset($dsn['host']) ? rawurldecode($dsn['host']) : '', + 'port' => isset($dsn['port']) ? rawurldecode($dsn['port']) : '', + 'username' => isset($dsn['user']) ? rawurldecode($dsn['user']) : '', + 'password' => isset($dsn['pass']) ? rawurldecode($dsn['pass']) : '', + 'database' => isset($dsn['path']) ? rawurldecode(substr($dsn['path'], 1)) : '' + ); + + // Were additional config items set? + if (isset($dsn['query'])) + { + parse_str($dsn['query'], $extra); + + foreach ($extra as $key => $val) + { + if (is_string($val) && in_array(strtoupper($val), array('TRUE', 'FALSE', 'NULL'))) + { + $val = var_export($val, TRUE); + } + + $params[$key] = $val; + } + } + } + + // No DB specified yet? Beat them senseless... + if (empty($params['dbdriver'])) + { + show_error('You have not selected a database type to connect to.'); + } + + // Load the DB classes. Note: Since the query builder class is optional + // we need to dynamically create a class that extends proper parent class + // based on whether we're using the query builder class or not. + if ($query_builder_override !== NULL) + { + $query_builder = $query_builder_override; + } + // Backwards compatibility work-around for keeping the + // $active_record config variable working. Should be + // removed in v3.1 + elseif ( ! isset($query_builder) && isset($active_record)) + { + $query_builder = $active_record; + } + + require_once(BASEPATH.'database/DB_driver.php'); + + if ( ! isset($query_builder) OR $query_builder === TRUE) + { + require_once(BASEPATH.'database/DB_query_builder.php'); + if ( ! class_exists('CI_DB', FALSE)) + { + /** + * CI_DB + * + * Acts as an alias for both CI_DB_driver and CI_DB_query_builder. + * + * @see CI_DB_query_builder + * @see CI_DB_driver + */ + class CI_DB extends CI_DB_query_builder { } + } + } + elseif ( ! class_exists('CI_DB', FALSE)) + { + /** + * @ignore + */ + class CI_DB extends CI_DB_driver { } + } + + // Load the DB driver + $driver_file = BASEPATH.'database/drivers/'.$params['dbdriver'].'/'.$params['dbdriver'].'_driver.php'; + + file_exists($driver_file) OR show_error('Invalid DB driver'); + require_once($driver_file); + + // Instantiate the DB adapter + $driver = 'CI_DB_'.$params['dbdriver'].'_driver'; + $DB = new $driver($params); + + // Check for a subdriver + if ( ! empty($DB->subdriver)) + { + $driver_file = BASEPATH.'database/drivers/'.$DB->dbdriver.'/subdrivers/'.$DB->dbdriver.'_'.$DB->subdriver.'_driver.php'; + + if (file_exists($driver_file)) + { + require_once($driver_file); + $driver = 'CI_DB_'.$DB->dbdriver.'_'.$DB->subdriver.'_driver'; + $DB = new $driver($params); + } + } + + $DB->initialize(); + return $DB; +} diff --git a/system/database/DB_cache.php b/system/database/DB_cache.php new file mode 100644 index 0000000..8855cc1 --- /dev/null +++ b/system/database/DB_cache.php @@ -0,0 +1,221 @@ +CI and load the file helper since we use it a lot + $this->CI =& get_instance(); + $this->db =& $db; + $this->CI->load->helper('file'); + + $this->check_path(); + } + + // -------------------------------------------------------------------- + + /** + * Set Cache Directory Path + * + * @param string $path Path to the cache directory + * @return bool + */ + public function check_path($path = '') + { + if ($path === '') + { + if ($this->db->cachedir === '') + { + return $this->db->cache_off(); + } + + $path = $this->db->cachedir; + } + + // Add a trailing slash to the path if needed + $path = realpath($path) + ? rtrim(realpath($path), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR + : rtrim($path, '/').'/'; + + if ( ! is_dir($path)) + { + log_message('debug', 'DB cache path error: '.$path); + + // If the path is wrong we'll turn off caching + return $this->db->cache_off(); + } + + if ( ! is_really_writable($path)) + { + log_message('debug', 'DB cache dir not writable: '.$path); + + // If the path is not really writable we'll turn off caching + return $this->db->cache_off(); + } + + $this->db->cachedir = $path; + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Retrieve a cached query + * + * The URI being requested will become the name of the cache sub-folder. + * An MD5 hash of the SQL statement will become the cache file name. + * + * @param string $sql + * @return string + */ + public function read($sql) + { + $segment_one = ($this->CI->uri->segment(1) == FALSE) ? 'default' : $this->CI->uri->segment(1); + $segment_two = ($this->CI->uri->segment(2) == FALSE) ? 'index' : $this->CI->uri->segment(2); + $filepath = $this->db->cachedir.$segment_one.'+'.$segment_two.'/'.md5($sql); + + if (FALSE === ($cachedata = @file_get_contents($filepath))) + { + return FALSE; + } + + return unserialize($cachedata); + } + + // -------------------------------------------------------------------- + + /** + * Write a query to a cache file + * + * @param string $sql + * @param object $object + * @return bool + */ + public function write($sql, $object) + { + $segment_one = ($this->CI->uri->segment(1) == FALSE) ? 'default' : $this->CI->uri->segment(1); + $segment_two = ($this->CI->uri->segment(2) == FALSE) ? 'index' : $this->CI->uri->segment(2); + $dir_path = $this->db->cachedir.$segment_one.'+'.$segment_two.'/'; + $filename = md5($sql); + + if ( ! is_dir($dir_path) && ! @mkdir($dir_path, 0750)) + { + return FALSE; + } + + if (write_file($dir_path.$filename, serialize($object)) === FALSE) + { + return FALSE; + } + + chmod($dir_path.$filename, 0640); + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Delete cache files within a particular directory + * + * @param string $segment_one + * @param string $segment_two + * @return void + */ + public function delete($segment_one = '', $segment_two = '') + { + if ($segment_one === '') + { + $segment_one = ($this->CI->uri->segment(1) == FALSE) ? 'default' : $this->CI->uri->segment(1); + } + + if ($segment_two === '') + { + $segment_two = ($this->CI->uri->segment(2) == FALSE) ? 'index' : $this->CI->uri->segment(2); + } + + $dir_path = $this->db->cachedir.$segment_one.'+'.$segment_two.'/'; + delete_files($dir_path, TRUE); + } + + // -------------------------------------------------------------------- + + /** + * Delete all existing cache files + * + * @return void + */ + public function delete_all() + { + delete_files($this->db->cachedir, TRUE, TRUE); + } + +} diff --git a/system/database/DB_driver.php b/system/database/DB_driver.php new file mode 100644 index 0000000..7ae52a3 --- /dev/null +++ b/system/database/DB_driver.php @@ -0,0 +1,1986 @@ + $val) + { + $this->$key = $val; + } + } + + log_message('info', 'Database Driver Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Initialize Database Settings + * + * @return bool + */ + public function initialize() + { + /* If an established connection is available, then there's + * no need to connect and select the database. + * + * Depending on the database driver, conn_id can be either + * boolean TRUE, a resource or an object. + */ + if ($this->conn_id) + { + return TRUE; + } + + // ---------------------------------------------------------------- + + // Connect to the database and set the connection ID + $this->conn_id = $this->db_connect($this->pconnect); + + // No connection resource? Check if there is a failover else throw an error + if ( ! $this->conn_id) + { + // Check if there is a failover set + if ( ! empty($this->failover) && is_array($this->failover)) + { + // Go over all the failovers + foreach ($this->failover as $failover) + { + // Replace the current settings with those of the failover + foreach ($failover as $key => $val) + { + $this->$key = $val; + } + + // Try to connect + $this->conn_id = $this->db_connect($this->pconnect); + + // If a connection is made break the foreach loop + if ($this->conn_id) + { + break; + } + } + } + + // We still don't have a connection? + if ( ! $this->conn_id) + { + log_message('error', 'Unable to connect to the database'); + + if ($this->db_debug) + { + $this->display_error('db_unable_to_connect'); + } + + return FALSE; + } + } + + // Now we set the character set and that's all + return $this->db_set_charset($this->char_set); + } + + // -------------------------------------------------------------------- + + /** + * DB connect + * + * This is just a dummy method that all drivers will override. + * + * @return mixed + */ + public function db_connect() + { + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Persistent database connection + * + * @return mixed + */ + public function db_pconnect() + { + return $this->db_connect(TRUE); + } + + // -------------------------------------------------------------------- + + /** + * Reconnect + * + * Keep / reestablish the db connection if no queries have been + * sent for a length of time exceeding the server's idle timeout. + * + * This is just a dummy method to allow drivers without such + * functionality to not declare it, while others will override it. + * + * @return void + */ + public function reconnect() + { + } + + // -------------------------------------------------------------------- + + /** + * Select database + * + * This is just a dummy method to allow drivers without such + * functionality to not declare it, while others will override it. + * + * @return bool + */ + public function db_select() + { + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Last error + * + * @return array + */ + public function error() + { + return array('code' => NULL, 'message' => NULL); + } + + // -------------------------------------------------------------------- + + /** + * Set client character set + * + * @param string + * @return bool + */ + public function db_set_charset($charset) + { + if (method_exists($this, '_db_set_charset') && ! $this->_db_set_charset($charset)) + { + log_message('error', 'Unable to set database connection charset: '.$charset); + + if ($this->db_debug) + { + $this->display_error('db_unable_to_set_charset', $charset); + } + + return FALSE; + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * The name of the platform in use (mysql, mssql, etc...) + * + * @return string + */ + public function platform() + { + return $this->dbdriver; + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * Returns a string containing the version of the database being used. + * Most drivers will override this method. + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + if (FALSE === ($sql = $this->_version())) + { + return ($this->db_debug) ? $this->display_error('db_unsupported_function') : FALSE; + } + + $query = $this->query($sql)->row(); + return $this->data_cache['version'] = $query->ver; + } + + // -------------------------------------------------------------------- + + /** + * Version number query string + * + * @return string + */ + protected function _version() + { + return 'SELECT VERSION() AS ver'; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * Accepts an SQL string as input and returns a result object upon + * successful execution of a "read" type query. Returns boolean TRUE + * upon successful execution of a "write" type query. Returns boolean + * FALSE upon failure, and if the $db_debug variable is set to TRUE + * will raise an error. + * + * @param string $sql + * @param array $binds = FALSE An array of binding data + * @param bool $return_object = NULL + * @return mixed + */ + public function query($sql, $binds = FALSE, $return_object = NULL) + { + if ($sql === '') + { + log_message('error', 'Invalid query: '.$sql); + return ($this->db_debug) ? $this->display_error('db_invalid_query') : FALSE; + } + elseif ( ! is_bool($return_object)) + { + $return_object = ! $this->is_write_type($sql); + } + + // Verify table prefix and replace if necessary + if ($this->dbprefix !== '' && $this->swap_pre !== '' && $this->dbprefix !== $this->swap_pre) + { + $sql = preg_replace('/(\W)'.$this->swap_pre.'(\S+?)/', '\\1'.$this->dbprefix.'\\2', $sql); + } + + // Compile binds if needed + if ($binds !== FALSE) + { + $sql = $this->compile_binds($sql, $binds); + } + + // Is query caching enabled? If the query is a "read type" + // we will load the caching class and return the previously + // cached query if it exists + if ($this->cache_on === TRUE && $return_object === TRUE && $this->_cache_init()) + { + $this->load_rdriver(); + if (FALSE !== ($cache = $this->CACHE->read($sql))) + { + return $cache; + } + } + + // Save the query for debugging + if ($this->save_queries === TRUE) + { + $this->queries[] = $sql; + } + + // Start the Query Timer + $time_start = microtime(TRUE); + + // Run the Query + if (FALSE === ($this->result_id = $this->simple_query($sql))) + { + if ($this->save_queries === TRUE) + { + $this->query_times[] = 0; + } + + // This will trigger a rollback if transactions are being used + if ($this->_trans_depth !== 0) + { + $this->_trans_status = FALSE; + } + + // Grab the error now, as we might run some additional queries before displaying the error + $error = $this->error(); + + // Log errors + log_message('error', 'Query error: '.$error['message'].' - Invalid query: '.$sql); + + if ($this->db_debug) + { + // We call this function in order to roll-back queries + // if transactions are enabled. If we don't call this here + // the error message will trigger an exit, causing the + // transactions to remain in limbo. + while ($this->_trans_depth !== 0) + { + $trans_depth = $this->_trans_depth; + $this->trans_complete(); + if ($trans_depth === $this->_trans_depth) + { + log_message('error', 'Database: Failure during an automated transaction commit/rollback!'); + break; + } + } + + // Display errors + return $this->display_error(array('Error Number: '.$error['code'], $error['message'], $sql)); + } + + return FALSE; + } + + // Stop and aggregate the query time results + $time_end = microtime(TRUE); + $this->benchmark += $time_end - $time_start; + + if ($this->save_queries === TRUE) + { + $this->query_times[] = $time_end - $time_start; + } + + // Increment the query counter + $this->query_count++; + + // Will we have a result object instantiated? If not - we'll simply return TRUE + if ($return_object !== TRUE) + { + // If caching is enabled we'll auto-cleanup any existing files related to this particular URI + if ($this->cache_on === TRUE && $this->cache_autodel === TRUE && $this->_cache_init()) + { + $this->CACHE->delete(); + } + + return TRUE; + } + + // Load and instantiate the result driver + $driver = $this->load_rdriver(); + $RES = new $driver($this); + + // Is query caching enabled? If so, we'll serialize the + // result object and save it to a cache file. + if ($this->cache_on === TRUE && $this->_cache_init()) + { + // We'll create a new instance of the result object + // only without the platform specific driver since + // we can't use it with cached data (the query result + // resource ID won't be any good once we've cached the + // result object, so we'll have to compile the data + // and save it) + $CR = new CI_DB_result($this); + $CR->result_object = $RES->result_object(); + $CR->result_array = $RES->result_array(); + $CR->num_rows = $RES->num_rows(); + + // Reset these since cached objects can not utilize resource IDs. + $CR->conn_id = NULL; + $CR->result_id = NULL; + + $this->CACHE->write($sql, $CR); + } + + return $RES; + } + + // -------------------------------------------------------------------- + + /** + * Load the result drivers + * + * @return string the name of the result class + */ + public function load_rdriver() + { + $driver = 'CI_DB_'.$this->dbdriver.'_result'; + + if ( ! class_exists($driver, FALSE)) + { + require_once(BASEPATH.'database/DB_result.php'); + require_once(BASEPATH.'database/drivers/'.$this->dbdriver.'/'.$this->dbdriver.'_result.php'); + } + + return $driver; + } + + // -------------------------------------------------------------------- + + /** + * Simple Query + * This is a simplified version of the query() function. Internally + * we only use it when running transaction commands since they do + * not require all the features of the main query() function. + * + * @param string the sql query + * @return mixed + */ + public function simple_query($sql) + { + if ( ! $this->conn_id) + { + if ( ! $this->initialize()) + { + return FALSE; + } + } + + return $this->_execute($sql); + } + + // -------------------------------------------------------------------- + + /** + * Disable Transactions + * This permits transactions to be disabled at run-time. + * + * @return void + */ + public function trans_off() + { + $this->trans_enabled = FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Enable/disable Transaction Strict Mode + * + * When strict mode is enabled, if you are running multiple groups of + * transactions, if one group fails all subsequent groups will be + * rolled back. + * + * If strict mode is disabled, each group is treated autonomously, + * meaning a failure of one group will not affect any others + * + * @param bool $mode = TRUE + * @return void + */ + public function trans_strict($mode = TRUE) + { + $this->trans_strict = is_bool($mode) ? $mode : TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Start Transaction + * + * @param bool $test_mode = FALSE + * @return bool + */ + public function trans_start($test_mode = FALSE) + { + if ( ! $this->trans_enabled) + { + return FALSE; + } + + return $this->trans_begin($test_mode); + } + + // -------------------------------------------------------------------- + + /** + * Complete Transaction + * + * @return bool + */ + public function trans_complete() + { + if ( ! $this->trans_enabled) + { + return FALSE; + } + + // The query() function will set this flag to FALSE in the event that a query failed + if ($this->_trans_status === FALSE OR $this->_trans_failure === TRUE) + { + $this->trans_rollback(); + + // If we are NOT running in strict mode, we will reset + // the _trans_status flag so that subsequent groups of + // transactions will be permitted. + if ($this->trans_strict === FALSE) + { + $this->_trans_status = TRUE; + } + + log_message('debug', 'DB Transaction Failure'); + return FALSE; + } + + return $this->trans_commit(); + } + + // -------------------------------------------------------------------- + + /** + * Lets you retrieve the transaction flag to determine if it has failed + * + * @return bool + */ + public function trans_status() + { + return $this->_trans_status; + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @param bool $test_mode + * @return bool + */ + public function trans_begin($test_mode = FALSE) + { + if ( ! $this->trans_enabled) + { + return FALSE; + } + // When transactions are nested we only begin/commit/rollback the outermost ones + elseif ($this->_trans_depth > 0) + { + $this->_trans_depth++; + return TRUE; + } + + // Reset the transaction failure flag. + // If the $test_mode flag is set to TRUE transactions will be rolled back + // even if the queries produce a successful result. + $this->_trans_failure = ($test_mode === TRUE); + + if ($this->_trans_begin()) + { + $this->_trans_depth++; + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + public function trans_commit() + { + if ( ! $this->trans_enabled OR $this->_trans_depth === 0) + { + return FALSE; + } + // When transactions are nested we only begin/commit/rollback the outermost ones + elseif ($this->_trans_depth > 1 OR $this->_trans_commit()) + { + $this->_trans_depth--; + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + public function trans_rollback() + { + if ( ! $this->trans_enabled OR $this->_trans_depth === 0) + { + return FALSE; + } + // When transactions are nested we only begin/commit/rollback the outermost ones + elseif ($this->_trans_depth > 1 OR $this->_trans_rollback()) + { + $this->_trans_depth--; + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Compile Bindings + * + * @param string the sql statement + * @param array an array of bind data + * @return string + */ + public function compile_binds($sql, $binds) + { + if (empty($this->bind_marker) OR strpos($sql, $this->bind_marker) === FALSE) + { + return $sql; + } + elseif ( ! is_array($binds)) + { + $binds = array($binds); + $bind_count = 1; + } + else + { + // Make sure we're using numeric keys + $binds = array_values($binds); + $bind_count = count($binds); + } + + // We'll need the marker length later + $ml = strlen($this->bind_marker); + + // Make sure not to replace a chunk inside a string that happens to match the bind marker + if ($c = preg_match_all("/'[^']*'/i", $sql, $matches)) + { + $c = preg_match_all('/'.preg_quote($this->bind_marker, '/').'/i', + str_replace($matches[0], + str_replace($this->bind_marker, str_repeat(' ', $ml), $matches[0]), + $sql, $c), + $matches, PREG_OFFSET_CAPTURE); + + // Bind values' count must match the count of markers in the query + if ($bind_count !== $c) + { + return $sql; + } + } + elseif (($c = preg_match_all('/'.preg_quote($this->bind_marker, '/').'/i', $sql, $matches, PREG_OFFSET_CAPTURE)) !== $bind_count) + { + return $sql; + } + + do + { + $c--; + $escaped_value = $this->escape($binds[$c]); + if (is_array($escaped_value)) + { + $escaped_value = '('.implode(',', $escaped_value).')'; + } + $sql = substr_replace($sql, $escaped_value, $matches[0][$c][1], $ml); + } + while ($c !== 0); + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Determines if a query is a "write" type. + * + * @param string An SQL query string + * @return bool + */ + public function is_write_type($sql) + { + return (bool) preg_match('/^\s*"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD|COPY|ALTER|RENAME|GRANT|REVOKE|LOCK|UNLOCK|REINDEX)\s/i', $sql); + } + + // -------------------------------------------------------------------- + + /** + * Calculate the aggregate query elapsed time + * + * @param int The number of decimal places + * @return string + */ + public function elapsed_time($decimals = 6) + { + return number_format($this->benchmark, $decimals); + } + + // -------------------------------------------------------------------- + + /** + * Returns the total number of queries + * + * @return int + */ + public function total_queries() + { + return $this->query_count; + } + + // -------------------------------------------------------------------- + + /** + * Returns the last query that was executed + * + * @return string + */ + public function last_query() + { + return end($this->queries); + } + + // -------------------------------------------------------------------- + + /** + * "Smart" Escape String + * + * Escapes data based on type + * Sets boolean and null types + * + * @param string + * @return mixed + */ + public function escape($str) + { + if (is_array($str)) + { + $str = array_map(array(&$this, 'escape'), $str); + return $str; + } + elseif (is_string($str) OR (is_object($str) && method_exists($str, '__toString'))) + { + return "'".$this->escape_str($str)."'"; + } + elseif (is_bool($str)) + { + return ($str === FALSE) ? 0 : 1; + } + elseif ($str === NULL) + { + return 'NULL'; + } + + return $str; + } + + // -------------------------------------------------------------------- + + /** + * Escape String + * + * @param string|string[] $str Input string + * @param bool $like Whether or not the string will be used in a LIKE condition + * @return string + */ + public function escape_str($str, $like = FALSE) + { + if (is_array($str)) + { + foreach ($str as $key => $val) + { + $str[$key] = $this->escape_str($val, $like); + } + + return $str; + } + + $str = $this->_escape_str($str); + + // escape LIKE condition wildcards + if ($like === TRUE) + { + return str_replace( + array($this->_like_escape_chr, '%', '_'), + array($this->_like_escape_chr.$this->_like_escape_chr, $this->_like_escape_chr.'%', $this->_like_escape_chr.'_'), + $str + ); + } + + return $str; + } + + // -------------------------------------------------------------------- + + /** + * Escape LIKE String + * + * Calls the individual driver for platform + * specific escaping for LIKE conditions + * + * @param string|string[] + * @return mixed + */ + public function escape_like_str($str) + { + return $this->escape_str($str, TRUE); + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependant string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + return str_replace("'", "''", remove_invisible_characters($str)); + } + + // -------------------------------------------------------------------- + + /** + * Primary + * + * Retrieves the primary key. It assumes that the row in the first + * position is the primary key + * + * @param string $table Table name + * @return string + */ + public function primary($table) + { + $fields = $this->list_fields($table); + return is_array($fields) ? current($fields) : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * "Count All" query + * + * Generates a platform-specific query string that counts all records in + * the specified database + * + * @param string + * @return int + */ + public function count_all($table = '') + { + if ($table === '') + { + return 0; + } + + $query = $this->query($this->_count_string.$this->escape_identifiers('numrows').' FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE)); + if ($query->num_rows() === 0) + { + return 0; + } + + $query = $query->row(); + $this->_reset_select(); + return (int) $query->numrows; + } + + // -------------------------------------------------------------------- + + /** + * Returns an array of table names + * + * @param string $constrain_by_prefix = FALSE + * @return array + */ + public function list_tables($constrain_by_prefix = FALSE) + { + // Is there a cached result? + if (isset($this->data_cache['table_names'])) + { + return $this->data_cache['table_names']; + } + + if (FALSE === ($sql = $this->_list_tables($constrain_by_prefix))) + { + return ($this->db_debug) ? $this->display_error('db_unsupported_function') : FALSE; + } + + $this->data_cache['table_names'] = array(); + $query = $this->query($sql); + + foreach ($query->result_array() as $row) + { + // Do we know from which column to get the table name? + if ( ! isset($key)) + { + if (isset($row['table_name'])) + { + $key = 'table_name'; + } + elseif (isset($row['TABLE_NAME'])) + { + $key = 'TABLE_NAME'; + } + else + { + /* We have no other choice but to just get the first element's key. + * Due to array_shift() accepting its argument by reference, if + * E_STRICT is on, this would trigger a warning. So we'll have to + * assign it first. + */ + $key = array_keys($row); + $key = array_shift($key); + } + } + + $this->data_cache['table_names'][] = $row[$key]; + } + + return $this->data_cache['table_names']; + } + + // -------------------------------------------------------------------- + + /** + * Determine if a particular table exists + * + * @param string $table_name + * @return bool + */ + public function table_exists($table_name) + { + return in_array($this->protect_identifiers($table_name, TRUE, FALSE, FALSE), $this->list_tables()); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * @param string $table Table name + * @return array + */ + public function list_fields($table) + { + // Is there a cached result? + if (isset($this->data_cache['field_names'][$table])) + { + return $this->data_cache['field_names'][$table]; + } + + if (FALSE === ($sql = $this->_list_columns($table))) + { + return ($this->db_debug) ? $this->display_error('db_unsupported_function') : FALSE; + } + + $query = $this->query($sql); + $this->data_cache['field_names'][$table] = array(); + + foreach ($query->result_array() as $row) + { + // Do we know from where to get the column's name? + if ( ! isset($key)) + { + if (isset($row['column_name'])) + { + $key = 'column_name'; + } + elseif (isset($row['COLUMN_NAME'])) + { + $key = 'COLUMN_NAME'; + } + else + { + // We have no other choice but to just get the first element's key. + $key = key($row); + } + } + + $this->data_cache['field_names'][$table][] = $row[$key]; + } + + return $this->data_cache['field_names'][$table]; + } + + // -------------------------------------------------------------------- + + /** + * Determine if a particular field exists + * + * @param string + * @param string + * @return bool + */ + public function field_exists($field_name, $table_name) + { + return in_array($field_name, $this->list_fields($table_name)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table the table name + * @return array + */ + public function field_data($table) + { + $query = $this->query($this->_field_data($this->protect_identifiers($table, TRUE, NULL, FALSE))); + return ($query) ? $query->field_data() : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Escape the SQL Identifiers + * + * This function escapes column and table names + * + * @param mixed + * @return mixed + */ + public function escape_identifiers($item) + { + if ($this->_escape_char === '' OR empty($item) OR in_array($item, $this->_reserved_identifiers)) + { + return $item; + } + elseif (is_array($item)) + { + foreach ($item as $key => $value) + { + $item[$key] = $this->escape_identifiers($value); + } + + return $item; + } + // Avoid breaking functions and literal values inside queries + elseif (ctype_digit($item) OR $item[0] === "'" OR ($this->_escape_char !== '"' && $item[0] === '"') OR strpos($item, '(') !== FALSE) + { + return $item; + } + + static $preg_ec = array(); + + if (empty($preg_ec)) + { + if (is_array($this->_escape_char)) + { + $preg_ec = array( + preg_quote($this->_escape_char[0], '/'), + preg_quote($this->_escape_char[1], '/'), + $this->_escape_char[0], + $this->_escape_char[1] + ); + } + else + { + $preg_ec[0] = $preg_ec[1] = preg_quote($this->_escape_char, '/'); + $preg_ec[2] = $preg_ec[3] = $this->_escape_char; + } + } + + foreach ($this->_reserved_identifiers as $id) + { + if (strpos($item, '.'.$id) !== FALSE) + { + return preg_replace('/'.$preg_ec[0].'?([^'.$preg_ec[1].'\.]+)'.$preg_ec[1].'?\./i', $preg_ec[2].'$1'.$preg_ec[3].'.', $item); + } + } + + return preg_replace('/'.$preg_ec[0].'?([^'.$preg_ec[1].'\.]+)'.$preg_ec[1].'?(\.)?/i', $preg_ec[2].'$1'.$preg_ec[3].'$2', $item); + } + + // -------------------------------------------------------------------- + + /** + * Generate an insert string + * + * @param string the table upon which the query will be performed + * @param array an associative array data of key/values + * @return string + */ + public function insert_string($table, $data) + { + $fields = $values = array(); + + foreach ($data as $key => $val) + { + $fields[] = $this->escape_identifiers($key); + $values[] = $this->escape($val); + } + + return $this->_insert($this->protect_identifiers($table, TRUE, NULL, FALSE), $fields, $values); + } + + // -------------------------------------------------------------------- + + /** + * Insert statement + * + * Generates a platform-specific insert string from the supplied data + * + * @param string the table name + * @param array the insert keys + * @param array the insert values + * @return string + */ + protected function _insert($table, $keys, $values) + { + return 'INSERT INTO '.$table.' ('.implode(', ', $keys).') VALUES ('.implode(', ', $values).')'; + } + + // -------------------------------------------------------------------- + + /** + * Generate an update string + * + * @param string the table upon which the query will be performed + * @param array an associative array data of key/values + * @param mixed the "where" statement + * @return string + */ + public function update_string($table, $data, $where) + { + if (empty($where)) + { + return FALSE; + } + + $this->where($where); + + $fields = array(); + foreach ($data as $key => $val) + { + $fields[$this->protect_identifiers($key)] = $this->escape($val); + } + + $sql = $this->_update($this->protect_identifiers($table, TRUE, NULL, FALSE), $fields); + $this->_reset_write(); + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string the table name + * @param array the update data + * @return string + */ + protected function _update($table, $values) + { + foreach ($values as $key => $val) + { + $valstr[] = $key.' = '.$val; + } + + return 'UPDATE '.$table.' SET '.implode(', ', $valstr) + .$this->_compile_wh('qb_where') + .$this->_compile_order_by() + .($this->qb_limit ? ' LIMIT '.$this->qb_limit : ''); + } + + // -------------------------------------------------------------------- + + /** + * Tests whether the string has an SQL operator + * + * @param string + * @return bool + */ + protected function _has_operator($str) + { + return (bool) preg_match('/(<|>|!|=|\sIS NULL|\sIS NOT NULL|\sEXISTS|\sBETWEEN|\sLIKE|\sIN\s*\(|\s)/i', trim($str)); + } + + // -------------------------------------------------------------------- + + /** + * Returns the SQL string operator + * + * @param string + * @return string + */ + protected function _get_operator($str) + { + static $_operators; + + if (empty($_operators)) + { + $_les = ($this->_like_escape_str !== '') + ? '\s+'.preg_quote(trim(sprintf($this->_like_escape_str, $this->_like_escape_chr)), '/') + : ''; + $_operators = array( + '\s*(?:<|>|!)?=\s*', // =, <=, >=, != + '\s*<>?\s*', // <, <> + '\s*>\s*', // > + '\s+IS NULL', // IS NULL + '\s+IS NOT NULL', // IS NOT NULL + '\s+EXISTS\s*\(.*\)', // EXISTS(sql) + '\s+NOT EXISTS\s*\(.*\)', // NOT EXISTS(sql) + '\s+BETWEEN\s+', // BETWEEN value AND value + '\s+IN\s*\(.*\)', // IN(list) + '\s+NOT IN\s*\(.*\)', // NOT IN (list) + '\s+LIKE\s+\S.*('.$_les.')?', // LIKE 'expr'[ ESCAPE '%s'] + '\s+NOT LIKE\s+\S.*('.$_les.')?' // NOT LIKE 'expr'[ ESCAPE '%s'] + ); + + } + + return preg_match('/'.implode('|', $_operators).'/i', $str, $match) + ? $match[0] : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Enables a native PHP function to be run, using a platform agnostic wrapper. + * + * @param string $function Function name + * @return mixed + */ + public function call_function($function) + { + $driver = ($this->dbdriver === 'postgre') ? 'pg_' : $this->dbdriver.'_'; + + if (FALSE === strpos($driver, $function)) + { + $function = $driver.$function; + } + + if ( ! function_exists($function)) + { + return ($this->db_debug) ? $this->display_error('db_unsupported_function') : FALSE; + } + + return (func_num_args() > 1) + ? call_user_func_array($function, array_slice(func_get_args(), 1)) + : call_user_func($function); + } + + // -------------------------------------------------------------------- + + /** + * Set Cache Directory Path + * + * @param string the path to the cache directory + * @return void + */ + public function cache_set_path($path = '') + { + $this->cachedir = $path; + } + + // -------------------------------------------------------------------- + + /** + * Enable Query Caching + * + * @return bool cache_on value + */ + public function cache_on() + { + return $this->cache_on = TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Disable Query Caching + * + * @return bool cache_on value + */ + public function cache_off() + { + return $this->cache_on = FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Delete the cache files associated with a particular URI + * + * @param string $segment_one = '' + * @param string $segment_two = '' + * @return bool + */ + public function cache_delete($segment_one = '', $segment_two = '') + { + return $this->_cache_init() + ? $this->CACHE->delete($segment_one, $segment_two) + : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Delete All cache files + * + * @return bool + */ + public function cache_delete_all() + { + return $this->_cache_init() + ? $this->CACHE->delete_all() + : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Initialize the Cache Class + * + * @return bool + */ + protected function _cache_init() + { + if ( ! class_exists('CI_DB_Cache', FALSE)) + { + require_once(BASEPATH.'database/DB_cache.php'); + } + elseif (is_object($this->CACHE)) + { + return TRUE; + } + + $this->CACHE = new CI_DB_Cache($this); // pass db object to support multiple db connections and returned db objects + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + public function close() + { + if ($this->conn_id) + { + $this->_close(); + $this->conn_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * This method would be overridden by most of the drivers. + * + * @return void + */ + protected function _close() + { + $this->conn_id = FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Display an error message + * + * @param string the error message + * @param string any "swap" values + * @param bool whether to localize the message + * @return string sends the application/views/errors/error_db.php template + */ + public function display_error($error = '', $swap = '', $native = FALSE) + { + $LANG =& load_class('Lang', 'core'); + $LANG->load('db'); + + $heading = $LANG->line('db_error_heading'); + + if ($native === TRUE) + { + $message = (array) $error; + } + else + { + $message = is_array($error) ? $error : array(str_replace('%s', $swap, $LANG->line($error))); + } + + // Find the most likely culprit of the error by going through + // the backtrace until the source file is no longer in the + // database folder. + $trace = debug_backtrace(); + foreach ($trace as $call) + { + if (isset($call['file'], $call['class'])) + { + // We'll need this on Windows, as APPPATH and BASEPATH will always use forward slashes + if (DIRECTORY_SEPARATOR !== '/') + { + $call['file'] = str_replace('\\', '/', $call['file']); + } + + if (strpos($call['file'], BASEPATH.'database') === FALSE && strpos($call['class'], 'Loader') === FALSE) + { + // Found it - use a relative path for safety + $message[] = 'Filename: '.str_replace(array(APPPATH, BASEPATH), '', $call['file']); + $message[] = 'Line Number: '.$call['line']; + break; + } + } + } + + $error =& load_class('Exceptions', 'core'); + echo $error->show_error($heading, $message, 'error_db'); + exit(8); // EXIT_DATABASE + } + + // -------------------------------------------------------------------- + + /** + * Protect Identifiers + * + * This function is used extensively by the Query Builder class, and by + * a couple functions in this class. + * It takes a column or table name (optionally with an alias) and inserts + * the table prefix onto it. Some logic is necessary in order to deal with + * column names that include the path. Consider a query like this: + * + * SELECT hostname.database.table.column AS c FROM hostname.database.table + * + * Or a query with aliasing: + * + * SELECT m.member_id, m.member_name FROM members AS m + * + * Since the column name can include up to four segments (host, DB, table, column) + * or also have an alias prefix, we need to do a bit of work to figure this out and + * insert the table prefix (if it exists) in the proper position, and escape only + * the correct identifiers. + * + * @param string + * @param bool + * @param mixed + * @param bool + * @return string + */ + public function protect_identifiers($item, $prefix_single = FALSE, $protect_identifiers = NULL, $field_exists = TRUE) + { + if ( ! is_bool($protect_identifiers)) + { + $protect_identifiers = $this->_protect_identifiers; + } + + if (is_array($item)) + { + $escaped_array = array(); + foreach ($item as $k => $v) + { + $escaped_array[$this->protect_identifiers($k)] = $this->protect_identifiers($v, $prefix_single, $protect_identifiers, $field_exists); + } + + return $escaped_array; + } + + // This is basically a bug fix for queries that use MAX, MIN, etc. + // If a parenthesis is found we know that we do not need to + // escape the data or add a prefix. There's probably a more graceful + // way to deal with this, but I'm not thinking of it -- Rick + // + // Added exception for single quotes as well, we don't want to alter + // literal strings. -- Narf + if (strcspn($item, "()'") !== strlen($item)) + { + return $item; + } + + // Convert tabs or multiple spaces into single spaces + $item = preg_replace('/\s+/', ' ', trim($item)); + + // If the item has an alias declaration we remove it and set it aside. + // Note: strripos() is used in order to support spaces in table names + if ($offset = strripos($item, ' AS ')) + { + $alias = ($protect_identifiers) + ? substr($item, $offset, 4).$this->escape_identifiers(substr($item, $offset + 4)) + : substr($item, $offset); + $item = substr($item, 0, $offset); + } + elseif ($offset = strrpos($item, ' ')) + { + $alias = ($protect_identifiers) + ? ' '.$this->escape_identifiers(substr($item, $offset + 1)) + : substr($item, $offset); + $item = substr($item, 0, $offset); + } + else + { + $alias = ''; + } + + // Break the string apart if it contains periods, then insert the table prefix + // in the correct location, assuming the period doesn't indicate that we're dealing + // with an alias. While we're at it, we will escape the components + if (strpos($item, '.') !== FALSE) + { + $parts = explode('.', $item); + + // Does the first segment of the exploded item match + // one of the aliases previously identified? If so, + // we have nothing more to do other than escape the item + // + // NOTE: The ! empty() condition prevents this method + // from breaking when QB isn't enabled. + if ( ! empty($this->qb_aliased_tables) && in_array($parts[0], $this->qb_aliased_tables)) + { + if ($protect_identifiers === TRUE) + { + foreach ($parts as $key => $val) + { + if ( ! in_array($val, $this->_reserved_identifiers)) + { + $parts[$key] = $this->escape_identifiers($val); + } + } + + $item = implode('.', $parts); + } + + return $item.$alias; + } + + // Is there a table prefix defined in the config file? If not, no need to do anything + if ($this->dbprefix !== '') + { + // We now add the table prefix based on some logic. + // Do we have 4 segments (hostname.database.table.column)? + // If so, we add the table prefix to the column name in the 3rd segment. + if (isset($parts[3])) + { + $i = 2; + } + // Do we have 3 segments (database.table.column)? + // If so, we add the table prefix to the column name in 2nd position + elseif (isset($parts[2])) + { + $i = 1; + } + // Do we have 2 segments (table.column)? + // If so, we add the table prefix to the column name in 1st segment + else + { + $i = 0; + } + + // This flag is set when the supplied $item does not contain a field name. + // This can happen when this function is being called from a JOIN. + if ($field_exists === FALSE) + { + $i++; + } + + // Verify table prefix and replace if necessary + if ($this->swap_pre !== '' && strpos($parts[$i], $this->swap_pre) === 0) + { + $parts[$i] = preg_replace('/^'.$this->swap_pre.'(\S+?)/', $this->dbprefix.'\\1', $parts[$i]); + } + // We only add the table prefix if it does not already exist + elseif (strpos($parts[$i], $this->dbprefix) !== 0) + { + $parts[$i] = $this->dbprefix.$parts[$i]; + } + + // Put the parts back together + $item = implode('.', $parts); + } + + if ($protect_identifiers === TRUE) + { + $item = $this->escape_identifiers($item); + } + + return $item.$alias; + } + + // Is there a table prefix? If not, no need to insert it + if ($this->dbprefix !== '') + { + // Verify table prefix and replace if necessary + if ($this->swap_pre !== '' && strpos($item, $this->swap_pre) === 0) + { + $item = preg_replace('/^'.$this->swap_pre.'(\S+?)/', $this->dbprefix.'\\1', $item); + } + // Do we prefix an item with no segments? + elseif ($prefix_single === TRUE && strpos($item, $this->dbprefix) !== 0) + { + $item = $this->dbprefix.$item; + } + } + + if ($protect_identifiers === TRUE && ! in_array($item, $this->_reserved_identifiers)) + { + $item = $this->escape_identifiers($item); + } + + return $item.$alias; + } + + // -------------------------------------------------------------------- + + /** + * Dummy method that allows Query Builder class to be disabled + * and keep count_all() working. + * + * @return void + */ + protected function _reset_select() + { + } + +} diff --git a/system/database/DB_forge.php b/system/database/DB_forge.php new file mode 100644 index 0000000..ed6f4b6 --- /dev/null +++ b/system/database/DB_forge.php @@ -0,0 +1,1032 @@ +db =& $db; + log_message('info', 'Database Forge Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Create database + * + * @param string $db_name + * @return bool + */ + public function create_database($db_name) + { + if ($this->_create_database === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + elseif ( ! $this->db->query(sprintf($this->_create_database, $this->db->escape_identifiers($db_name), $this->db->char_set, $this->db->dbcollat))) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + + if ( ! empty($this->db->data_cache['db_names'])) + { + $this->db->data_cache['db_names'][] = $db_name; + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Drop database + * + * @param string $db_name + * @return bool + */ + public function drop_database($db_name) + { + if ($this->_drop_database === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + elseif ( ! $this->db->query(sprintf($this->_drop_database, $this->db->escape_identifiers($db_name)))) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + + if ( ! empty($this->db->data_cache['db_names'])) + { + $key = array_search(strtolower($db_name), array_map('strtolower', $this->db->data_cache['db_names']), TRUE); + if ($key !== FALSE) + { + unset($this->db->data_cache['db_names'][$key]); + } + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Add Key + * + * @param string $key + * @param bool $primary + * @return CI_DB_forge + */ + public function add_key($key, $primary = FALSE) + { + // DO NOT change this! This condition is only applicable + // for PRIMARY keys because you can only have one such, + // and therefore all fields you add to it will be included + // in the same, composite PRIMARY KEY. + // + // It's not the same for regular indexes. + if ($primary === TRUE && is_array($key)) + { + foreach ($key as $one) + { + $this->add_key($one, $primary); + } + + return $this; + } + + if ($primary === TRUE) + { + $this->primary_keys[] = $key; + } + else + { + $this->keys[] = $key; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Add Field + * + * @param array $field + * @return CI_DB_forge + */ + public function add_field($field) + { + if (is_string($field)) + { + if ($field === 'id') + { + $this->add_field(array( + 'id' => array( + 'type' => 'INT', + 'constraint' => 9, + 'auto_increment' => TRUE + ) + )); + $this->add_key('id', TRUE); + } + else + { + if (strpos($field, ' ') === FALSE) + { + show_error('Field information is required for that operation.'); + } + + $this->fields[] = $field; + } + } + + if (is_array($field)) + { + $this->fields = array_merge($this->fields, $field); + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Create Table + * + * @param string $table Table name + * @param bool $if_not_exists Whether to add IF NOT EXISTS condition + * @param array $attributes Associative array of table attributes + * @return bool + */ + public function create_table($table, $if_not_exists = FALSE, array $attributes = array()) + { + if ($table === '') + { + show_error('A table name is required for that operation.'); + } + else + { + $table = $this->db->dbprefix.$table; + } + + if (count($this->fields) === 0) + { + show_error('Field information is required.'); + } + + $sql = $this->_create_table($table, $if_not_exists, $attributes); + + if (is_bool($sql)) + { + $this->_reset(); + if ($sql === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + } + + if (($result = $this->db->query($sql)) !== FALSE) + { + empty($this->db->data_cache['table_names']) OR $this->db->data_cache['table_names'][] = $table; + + // Most databases don't support creating indexes from within the CREATE TABLE statement + if ( ! empty($this->keys)) + { + for ($i = 0, $sqls = $this->_process_indexes($table), $c = count($sqls); $i < $c; $i++) + { + $this->db->query($sqls[$i]); + } + } + } + + $this->_reset(); + return $result; + } + + // -------------------------------------------------------------------- + + /** + * Create Table + * + * @param string $table Table name + * @param bool $if_not_exists Whether to add 'IF NOT EXISTS' condition + * @param array $attributes Associative array of table attributes + * @return mixed + */ + protected function _create_table($table, $if_not_exists, $attributes) + { + if ($if_not_exists === TRUE && $this->_create_table_if === FALSE) + { + if ($this->db->table_exists($table)) + { + return TRUE; + } + else + { + $if_not_exists = FALSE; + } + } + + $sql = ($if_not_exists) + ? sprintf($this->_create_table_if, $this->db->escape_identifiers($table)) + : 'CREATE TABLE'; + + $columns = $this->_process_fields(TRUE); + for ($i = 0, $c = count($columns); $i < $c; $i++) + { + $columns[$i] = ($columns[$i]['_literal'] !== FALSE) + ? "\n\t".$columns[$i]['_literal'] + : "\n\t".$this->_process_column($columns[$i]); + } + + $columns = implode(',', $columns) + .$this->_process_primary_keys($table); + + // Are indexes created from within the CREATE TABLE statement? (e.g. in MySQL) + if ($this->_create_table_keys === TRUE) + { + $columns .= $this->_process_indexes($table); + } + + // _create_table will usually have the following format: "%s %s (%s\n)" + $sql = sprintf($this->_create_table.'%s', + $sql, + $this->db->escape_identifiers($table), + $columns, + $this->_create_table_attr($attributes) + ); + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * CREATE TABLE attributes + * + * @param array $attributes Associative array of table attributes + * @return string + */ + protected function _create_table_attr($attributes) + { + $sql = ''; + + foreach (array_keys($attributes) as $key) + { + if (is_string($key)) + { + $sql .= ' '.strtoupper($key).' '.$attributes[$key]; + } + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Drop Table + * + * @param string $table_name Table name + * @param bool $if_exists Whether to add an IF EXISTS condition + * @return bool + */ + public function drop_table($table_name, $if_exists = FALSE) + { + if ($table_name === '') + { + return ($this->db->db_debug) ? $this->db->display_error('db_table_name_required') : FALSE; + } + + if (($query = $this->_drop_table($this->db->dbprefix.$table_name, $if_exists)) === TRUE) + { + return TRUE; + } + + $query = $this->db->query($query); + + // Update table list cache + if ($query && ! empty($this->db->data_cache['table_names'])) + { + $key = array_search(strtolower($this->db->dbprefix.$table_name), array_map('strtolower', $this->db->data_cache['table_names']), TRUE); + if ($key !== FALSE) + { + unset($this->db->data_cache['table_names'][$key]); + } + } + + return $query; + } + + // -------------------------------------------------------------------- + + /** + * Drop Table + * + * Generates a platform-specific DROP TABLE string + * + * @param string $table Table name + * @param bool $if_exists Whether to add an IF EXISTS condition + * @return string + */ + protected function _drop_table($table, $if_exists) + { + $sql = 'DROP TABLE'; + + if ($if_exists) + { + if ($this->_drop_table_if === FALSE) + { + if ( ! $this->db->table_exists($table)) + { + return TRUE; + } + } + else + { + $sql = sprintf($this->_drop_table_if, $this->db->escape_identifiers($table)); + } + } + + return $sql.' '.$this->db->escape_identifiers($table); + } + + // -------------------------------------------------------------------- + + /** + * Rename Table + * + * @param string $table_name Old table name + * @param string $new_table_name New table name + * @return bool + */ + public function rename_table($table_name, $new_table_name) + { + if ($table_name === '' OR $new_table_name === '') + { + show_error('A table name is required for that operation.'); + return FALSE; + } + elseif ($this->_rename_table === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + $result = $this->db->query(sprintf($this->_rename_table, + $this->db->escape_identifiers($this->db->dbprefix.$table_name), + $this->db->escape_identifiers($this->db->dbprefix.$new_table_name)) + ); + + if ($result && ! empty($this->db->data_cache['table_names'])) + { + $key = array_search(strtolower($this->db->dbprefix.$table_name), array_map('strtolower', $this->db->data_cache['table_names']), TRUE); + if ($key !== FALSE) + { + $this->db->data_cache['table_names'][$key] = $this->db->dbprefix.$new_table_name; + } + } + + return $result; + } + + // -------------------------------------------------------------------- + + /** + * Column Add + * + * @todo Remove deprecated $_after option in 3.1+ + * @param string $table Table name + * @param array $field Column definition + * @param string $_after Column for AFTER clause (deprecated) + * @return bool + */ + public function add_column($table, $field, $_after = NULL) + { + // Work-around for literal column definitions + is_array($field) OR $field = array($field); + + foreach (array_keys($field) as $k) + { + // Backwards-compatibility work-around for MySQL/CUBRID AFTER clause (remove in 3.1+) + if ($_after !== NULL && is_array($field[$k]) && ! isset($field[$k]['after'])) + { + $field[$k]['after'] = $_after; + } + + $this->add_field(array($k => $field[$k])); + } + + $sqls = $this->_alter_table('ADD', $this->db->dbprefix.$table, $this->_process_fields()); + $this->_reset(); + if ($sqls === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + for ($i = 0, $c = count($sqls); $i < $c; $i++) + { + if ($this->db->query($sqls[$i]) === FALSE) + { + return FALSE; + } + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Column Drop + * + * @param string $table Table name + * @param string $column_name Column name + * @return bool + */ + public function drop_column($table, $column_name) + { + $sql = $this->_alter_table('DROP', $this->db->dbprefix.$table, $column_name); + if ($sql === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + return $this->db->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Column Modify + * + * @param string $table Table name + * @param string $field Column definition + * @return bool + */ + public function modify_column($table, $field) + { + // Work-around for literal column definitions + is_array($field) OR $field = array($field); + + foreach (array_keys($field) as $k) + { + $this->add_field(array($k => $field[$k])); + } + + if (count($this->fields) === 0) + { + show_error('Field information is required.'); + } + + $sqls = $this->_alter_table('CHANGE', $this->db->dbprefix.$table, $this->_process_fields()); + $this->_reset(); + if ($sqls === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + for ($i = 0, $c = count($sqls); $i < $c; $i++) + { + if ($this->db->query($sqls[$i]) === FALSE) + { + return FALSE; + } + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' '; + + // DROP has everything it needs now. + if ($alter_type === 'DROP') + { + return $sql.'DROP COLUMN '.$this->db->escape_identifiers($field); + } + + $sql .= ($alter_type === 'ADD') + ? 'ADD ' + : $alter_type.' COLUMN '; + + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + $sqls[] = $sql + .($field[$i]['_literal'] !== FALSE ? $field[$i]['_literal'] : $this->_process_column($field[$i])); + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Process fields + * + * @param bool $create_table + * @return array + */ + protected function _process_fields($create_table = FALSE) + { + $fields = array(); + + foreach ($this->fields as $key => $attributes) + { + if (is_int($key) && ! is_array($attributes)) + { + $fields[] = array('_literal' => $attributes); + continue; + } + + $attributes = array_change_key_case($attributes, CASE_UPPER); + + if ($create_table === TRUE && empty($attributes['TYPE'])) + { + continue; + } + + isset($attributes['TYPE']) && $this->_attr_type($attributes); + + $field = array( + 'name' => $key, + 'new_name' => isset($attributes['NAME']) ? $attributes['NAME'] : NULL, + 'type' => isset($attributes['TYPE']) ? $attributes['TYPE'] : NULL, + 'length' => '', + 'unsigned' => '', + 'null' => '', + 'unique' => '', + 'default' => '', + 'auto_increment' => '', + '_literal' => FALSE + ); + + isset($attributes['TYPE']) && $this->_attr_unsigned($attributes, $field); + + if ($create_table === FALSE) + { + if (isset($attributes['AFTER'])) + { + $field['after'] = $attributes['AFTER']; + } + elseif (isset($attributes['FIRST'])) + { + $field['first'] = (bool) $attributes['FIRST']; + } + } + + $this->_attr_default($attributes, $field); + + if (isset($attributes['NULL'])) + { + if ($attributes['NULL'] === TRUE) + { + $field['null'] = empty($this->_null) ? '' : ' '.$this->_null; + } + else + { + $field['null'] = ' NOT NULL'; + } + } + elseif ($create_table === TRUE) + { + $field['null'] = ' NOT NULL'; + } + + $this->_attr_auto_increment($attributes, $field); + $this->_attr_unique($attributes, $field); + + if (isset($attributes['COMMENT'])) + { + $field['comment'] = $this->db->escape($attributes['COMMENT']); + } + + if (isset($attributes['TYPE']) && ! empty($attributes['CONSTRAINT'])) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'ENUM': + case 'SET': + $attributes['CONSTRAINT'] = $this->db->escape($attributes['CONSTRAINT']); + default: + $field['length'] = is_array($attributes['CONSTRAINT']) + ? '('.implode(',', $attributes['CONSTRAINT']).')' + : '('.$attributes['CONSTRAINT'].')'; + break; + } + } + + $fields[] = $field; + } + + return $fields; + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'].$field['length'] + .$field['unsigned'] + .$field['default'] + .$field['null'] + .$field['auto_increment'] + .$field['unique']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + // Usually overridden by drivers + } + + // -------------------------------------------------------------------- + + /** + * Field attribute UNSIGNED + * + * Depending on the _unsigned property value: + * + * - TRUE will always set $field['unsigned'] to 'UNSIGNED' + * - FALSE will always set $field['unsigned'] to '' + * - array(TYPE) will set $field['unsigned'] to 'UNSIGNED', + * if $attributes['TYPE'] is found in the array + * - array(TYPE => UTYPE) will change $field['type'], + * from TYPE to UTYPE in case of a match + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_unsigned(&$attributes, &$field) + { + if (empty($attributes['UNSIGNED']) OR $attributes['UNSIGNED'] !== TRUE) + { + return; + } + + // Reset the attribute in order to avoid issues if we do type conversion + $attributes['UNSIGNED'] = FALSE; + + if (is_array($this->_unsigned)) + { + foreach (array_keys($this->_unsigned) as $key) + { + if (is_int($key) && strcasecmp($attributes['TYPE'], $this->_unsigned[$key]) === 0) + { + $field['unsigned'] = ' UNSIGNED'; + return; + } + elseif (is_string($key) && strcasecmp($attributes['TYPE'], $key) === 0) + { + $field['type'] = $key; + return; + } + } + + return; + } + + $field['unsigned'] = ($this->_unsigned === TRUE) ? ' UNSIGNED' : ''; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute DEFAULT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_default(&$attributes, &$field) + { + if ($this->_default === FALSE) + { + return; + } + + if (array_key_exists('DEFAULT', $attributes)) + { + if ($attributes['DEFAULT'] === NULL) + { + $field['default'] = empty($this->_null) ? '' : $this->_default.$this->_null; + + // Override the NULL attribute if that's our default + $attributes['NULL'] = TRUE; + $field['null'] = empty($this->_null) ? '' : ' '.$this->_null; + } + else + { + $field['default'] = $this->_default.$this->db->escape($attributes['DEFAULT']); + } + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute UNIQUE + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_unique(&$attributes, &$field) + { + if ( ! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === TRUE) + { + $field['unique'] = ' UNIQUE'; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['auto_increment'] = ' AUTO_INCREMENT'; + } + } + + // -------------------------------------------------------------------- + + /** + * Process primary keys + * + * @param string $table Table name + * @return string + */ + protected function _process_primary_keys($table) + { + $sql = ''; + + for ($i = 0, $c = count($this->primary_keys); $i < $c; $i++) + { + if ( ! isset($this->fields[$this->primary_keys[$i]])) + { + unset($this->primary_keys[$i]); + } + } + + if (count($this->primary_keys) > 0) + { + $sql .= ",\n\tCONSTRAINT ".$this->db->escape_identifiers('pk_'.$table) + .' PRIMARY KEY('.implode(', ', $this->db->escape_identifiers($this->primary_keys)).')'; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Process indexes + * + * @param string $table + * @return string + */ + protected function _process_indexes($table) + { + $sqls = array(); + + for ($i = 0, $c = count($this->keys); $i < $c; $i++) + { + if (is_array($this->keys[$i])) + { + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) + { + if ( ! isset($this->fields[$this->keys[$i][$i2]])) + { + unset($this->keys[$i][$i2]); + continue; + } + } + } + elseif ( ! isset($this->fields[$this->keys[$i]])) + { + unset($this->keys[$i]); + continue; + } + + is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]); + + $sqls[] = 'CREATE INDEX '.$this->db->escape_identifiers($table.'_'.implode('_', $this->keys[$i])) + .' ON '.$this->db->escape_identifiers($table) + .' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).');'; + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Reset + * + * Resets table creation vars + * + * @return void + */ + protected function _reset() + { + $this->fields = $this->keys = $this->primary_keys = array(); + } + +} diff --git a/system/database/DB_query_builder.php b/system/database/DB_query_builder.php new file mode 100644 index 0000000..5a86ce5 --- /dev/null +++ b/system/database/DB_query_builder.php @@ -0,0 +1,2789 @@ +_protect_identifiers; + + foreach ($select as $val) + { + $val = trim($val); + + if ($val !== '') + { + $this->qb_select[] = $val; + $this->qb_no_escape[] = $escape; + + if ($this->qb_caching === TRUE) + { + $this->qb_cache_select[] = $val; + $this->qb_cache_exists[] = 'select'; + $this->qb_cache_no_escape[] = $escape; + } + } + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Select Max + * + * Generates a SELECT MAX(field) portion of a query + * + * @param string the field + * @param string an alias + * @return CI_DB_query_builder + */ + public function select_max($select = '', $alias = '') + { + return $this->_max_min_avg_sum($select, $alias, 'MAX'); + } + + // -------------------------------------------------------------------- + + /** + * Select Min + * + * Generates a SELECT MIN(field) portion of a query + * + * @param string the field + * @param string an alias + * @return CI_DB_query_builder + */ + public function select_min($select = '', $alias = '') + { + return $this->_max_min_avg_sum($select, $alias, 'MIN'); + } + + // -------------------------------------------------------------------- + + /** + * Select Average + * + * Generates a SELECT AVG(field) portion of a query + * + * @param string the field + * @param string an alias + * @return CI_DB_query_builder + */ + public function select_avg($select = '', $alias = '') + { + return $this->_max_min_avg_sum($select, $alias, 'AVG'); + } + + // -------------------------------------------------------------------- + + /** + * Select Sum + * + * Generates a SELECT SUM(field) portion of a query + * + * @param string the field + * @param string an alias + * @return CI_DB_query_builder + */ + public function select_sum($select = '', $alias = '') + { + return $this->_max_min_avg_sum($select, $alias, 'SUM'); + } + + // -------------------------------------------------------------------- + + /** + * SELECT [MAX|MIN|AVG|SUM]() + * + * @used-by select_max() + * @used-by select_min() + * @used-by select_avg() + * @used-by select_sum() + * + * @param string $select Field name + * @param string $alias + * @param string $type + * @return CI_DB_query_builder + */ + protected function _max_min_avg_sum($select = '', $alias = '', $type = 'MAX') + { + if ( ! is_string($select) OR $select === '') + { + $this->display_error('db_invalid_query'); + } + + $type = strtoupper($type); + + if ( ! in_array($type, array('MAX', 'MIN', 'AVG', 'SUM'))) + { + show_error('Invalid function type: '.$type); + } + + if ($alias === '') + { + $alias = $this->_create_alias_from_table(trim($select)); + } + + $sql = $type.'('.$this->protect_identifiers(trim($select)).') AS '.$this->escape_identifiers(trim($alias)); + + $this->qb_select[] = $sql; + $this->qb_no_escape[] = NULL; + + if ($this->qb_caching === TRUE) + { + $this->qb_cache_select[] = $sql; + $this->qb_cache_exists[] = 'select'; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Determines the alias name based on the table + * + * @param string $item + * @return string + */ + protected function _create_alias_from_table($item) + { + if (strpos($item, '.') !== FALSE) + { + $item = explode('.', $item); + return end($item); + } + + return $item; + } + + // -------------------------------------------------------------------- + + /** + * DISTINCT + * + * Sets a flag which tells the query string compiler to add DISTINCT + * + * @param bool $val + * @return CI_DB_query_builder + */ + public function distinct($val = TRUE) + { + $this->qb_distinct = is_bool($val) ? $val : TRUE; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * From + * + * Generates the FROM portion of the query + * + * @param mixed $from can be a string or array + * @return CI_DB_query_builder + */ + public function from($from) + { + foreach ((array) $from as $val) + { + if (strpos($val, ',') !== FALSE) + { + foreach (explode(',', $val) as $v) + { + $v = trim($v); + $this->_track_aliases($v); + + $this->qb_from[] = $v = $this->protect_identifiers($v, TRUE, NULL, FALSE); + + if ($this->qb_caching === TRUE) + { + $this->qb_cache_from[] = $v; + $this->qb_cache_exists[] = 'from'; + } + } + } + else + { + $val = trim($val); + + // Extract any aliases that might exist. We use this information + // in the protect_identifiers to know whether to add a table prefix + $this->_track_aliases($val); + + $this->qb_from[] = $val = $this->protect_identifiers($val, TRUE, NULL, FALSE); + + if ($this->qb_caching === TRUE) + { + $this->qb_cache_from[] = $val; + $this->qb_cache_exists[] = 'from'; + } + } + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * JOIN + * + * Generates the JOIN portion of the query + * + * @param string + * @param string the join condition + * @param string the type of join + * @param string whether not to try to escape identifiers + * @return CI_DB_query_builder + */ + public function join($table, $cond, $type = '', $escape = NULL) + { + if ($type !== '') + { + $type = strtoupper(trim($type)); + + if ( ! in_array($type, array('LEFT', 'RIGHT', 'OUTER', 'INNER', 'LEFT OUTER', 'RIGHT OUTER'), TRUE)) + { + $type = ''; + } + else + { + $type .= ' '; + } + } + + // Extract any aliases that might exist. We use this information + // in the protect_identifiers to know whether to add a table prefix + $this->_track_aliases($table); + + is_bool($escape) OR $escape = $this->_protect_identifiers; + + if ( ! $this->_has_operator($cond)) + { + $cond = ' USING ('.($escape ? $this->escape_identifiers($cond) : $cond).')'; + } + elseif ($escape === FALSE) + { + $cond = ' ON '.$cond; + } + else + { + // Split multiple conditions + if (preg_match_all('/\sAND\s|\sOR\s/i', $cond, $joints, PREG_OFFSET_CAPTURE)) + { + $conditions = array(); + $joints = $joints[0]; + array_unshift($joints, array('', 0)); + + for ($i = count($joints) - 1, $pos = strlen($cond); $i >= 0; $i--) + { + $joints[$i][1] += strlen($joints[$i][0]); // offset + $conditions[$i] = substr($cond, $joints[$i][1], $pos - $joints[$i][1]); + $pos = $joints[$i][1] - strlen($joints[$i][0]); + $joints[$i] = $joints[$i][0]; + } + } + else + { + $conditions = array($cond); + $joints = array(''); + } + + $cond = ' ON '; + for ($i = 0, $c = count($conditions); $i < $c; $i++) + { + $operator = $this->_get_operator($conditions[$i]); + $cond .= $joints[$i]; + $cond .= preg_match("/(\(*)?([\[\]\w\.'-]+)".preg_quote($operator)."(.*)/i", $conditions[$i], $match) + ? $match[1].$this->protect_identifiers($match[2]).$operator.$this->protect_identifiers($match[3]) + : $conditions[$i]; + } + } + + // Do we want to escape the table name? + if ($escape === TRUE) + { + $table = $this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + // Assemble the JOIN statement + $this->qb_join[] = $join = $type.'JOIN '.$table.$cond; + + if ($this->qb_caching === TRUE) + { + $this->qb_cache_join[] = $join; + $this->qb_cache_exists[] = 'join'; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * WHERE + * + * Generates the WHERE portion of the query. + * Separates multiple calls with 'AND'. + * + * @param mixed + * @param mixed + * @param bool + * @return CI_DB_query_builder + */ + public function where($key, $value = NULL, $escape = NULL) + { + return $this->_wh('qb_where', $key, $value, 'AND ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * OR WHERE + * + * Generates the WHERE portion of the query. + * Separates multiple calls with 'OR'. + * + * @param mixed + * @param mixed + * @param bool + * @return CI_DB_query_builder + */ + public function or_where($key, $value = NULL, $escape = NULL) + { + return $this->_wh('qb_where', $key, $value, 'OR ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * WHERE, HAVING + * + * @used-by where() + * @used-by or_where() + * @used-by having() + * @used-by or_having() + * + * @param string $qb_key 'qb_where' or 'qb_having' + * @param mixed $key + * @param mixed $value + * @param string $type + * @param bool $escape + * @return CI_DB_query_builder + */ + protected function _wh($qb_key, $key, $value = NULL, $type = 'AND ', $escape = NULL) + { + $qb_cache_key = ($qb_key === 'qb_having') ? 'qb_cache_having' : 'qb_cache_where'; + + if ( ! is_array($key)) + { + $key = array($key => $value); + } + + // If the escape value was not set will base it on the global setting + is_bool($escape) OR $escape = $this->_protect_identifiers; + + foreach ($key as $k => $v) + { + $prefix = (count($this->$qb_key) === 0 && count($this->$qb_cache_key) === 0) + ? $this->_group_get_type('') + : $this->_group_get_type($type); + + if ($v !== NULL) + { + if ($escape === TRUE) + { + $v = ' '.$this->escape($v); + } + + if ( ! $this->_has_operator($k)) + { + $k .= ' = '; + } + } + elseif ( ! $this->_has_operator($k)) + { + // value appears not to have been set, assign the test to IS NULL + $k .= ' IS NULL'; + } + elseif (preg_match('/\s*(!?=|<>|\sIS(?:\s+NOT)?\s)\s*$/i', $k, $match, PREG_OFFSET_CAPTURE)) + { + $k = substr($k, 0, $match[0][1]).($match[1][0] === '=' ? ' IS NULL' : ' IS NOT NULL'); + } + + $this->{$qb_key}[] = array('condition' => $prefix.$k.$v, 'escape' => $escape); + if ($this->qb_caching === TRUE) + { + $this->{$qb_cache_key}[] = array('condition' => $prefix.$k.$v, 'escape' => $escape); + $this->qb_cache_exists[] = substr($qb_key, 3); + } + + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * WHERE IN + * + * Generates a WHERE field IN('item', 'item') SQL query, + * joined with 'AND' if appropriate. + * + * @param string $key The field to search + * @param array $values The values searched on + * @param bool $escape + * @return CI_DB_query_builder + */ + public function where_in($key = NULL, $values = NULL, $escape = NULL) + { + return $this->_where_in($key, $values, FALSE, 'AND ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * OR WHERE IN + * + * Generates a WHERE field IN('item', 'item') SQL query, + * joined with 'OR' if appropriate. + * + * @param string $key The field to search + * @param array $values The values searched on + * @param bool $escape + * @return CI_DB_query_builder + */ + public function or_where_in($key = NULL, $values = NULL, $escape = NULL) + { + return $this->_where_in($key, $values, FALSE, 'OR ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * WHERE NOT IN + * + * Generates a WHERE field NOT IN('item', 'item') SQL query, + * joined with 'AND' if appropriate. + * + * @param string $key The field to search + * @param array $values The values searched on + * @param bool $escape + * @return CI_DB_query_builder + */ + public function where_not_in($key = NULL, $values = NULL, $escape = NULL) + { + return $this->_where_in($key, $values, TRUE, 'AND ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * OR WHERE NOT IN + * + * Generates a WHERE field NOT IN('item', 'item') SQL query, + * joined with 'OR' if appropriate. + * + * @param string $key The field to search + * @param array $values The values searched on + * @param bool $escape + * @return CI_DB_query_builder + */ + public function or_where_not_in($key = NULL, $values = NULL, $escape = NULL) + { + return $this->_where_in($key, $values, TRUE, 'OR ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * Internal WHERE IN + * + * @used-by where_in() + * @used-by or_where_in() + * @used-by where_not_in() + * @used-by or_where_not_in() + * + * @param string $key The field to search + * @param array $values The values searched on + * @param bool $not If the statement would be IN or NOT IN + * @param string $type + * @param bool $escape + * @return CI_DB_query_builder + */ + protected function _where_in($key = NULL, $values = NULL, $not = FALSE, $type = 'AND ', $escape = NULL) + { + if ($key === NULL OR $values === NULL) + { + return $this; + } + + if ( ! is_array($values)) + { + $values = array($values); + } + + is_bool($escape) OR $escape = $this->_protect_identifiers; + + $not = ($not) ? ' NOT' : ''; + + if ($escape === TRUE) + { + $where_in = array(); + foreach ($values as $value) + { + $where_in[] = $this->escape($value); + } + } + else + { + $where_in = array_values($values); + } + + $prefix = (count($this->qb_where) === 0 && count($this->qb_cache_where) === 0) + ? $this->_group_get_type('') + : $this->_group_get_type($type); + + $where_in = array( + 'condition' => $prefix.$key.$not.' IN('.implode(', ', $where_in).')', + 'escape' => $escape + ); + + $this->qb_where[] = $where_in; + if ($this->qb_caching === TRUE) + { + $this->qb_cache_where[] = $where_in; + $this->qb_cache_exists[] = 'where'; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * LIKE + * + * Generates a %LIKE% portion of the query. + * Separates multiple calls with 'AND'. + * + * @param mixed $field + * @param string $match + * @param string $side + * @param bool $escape + * @return CI_DB_query_builder + */ + public function like($field, $match = '', $side = 'both', $escape = NULL) + { + return $this->_like($field, $match, 'AND ', $side, '', $escape); + } + + // -------------------------------------------------------------------- + + /** + * NOT LIKE + * + * Generates a NOT LIKE portion of the query. + * Separates multiple calls with 'AND'. + * + * @param mixed $field + * @param string $match + * @param string $side + * @param bool $escape + * @return CI_DB_query_builder + */ + public function not_like($field, $match = '', $side = 'both', $escape = NULL) + { + return $this->_like($field, $match, 'AND ', $side, 'NOT', $escape); + } + + // -------------------------------------------------------------------- + + /** + * OR LIKE + * + * Generates a %LIKE% portion of the query. + * Separates multiple calls with 'OR'. + * + * @param mixed $field + * @param string $match + * @param string $side + * @param bool $escape + * @return CI_DB_query_builder + */ + public function or_like($field, $match = '', $side = 'both', $escape = NULL) + { + return $this->_like($field, $match, 'OR ', $side, '', $escape); + } + + // -------------------------------------------------------------------- + + /** + * OR NOT LIKE + * + * Generates a NOT LIKE portion of the query. + * Separates multiple calls with 'OR'. + * + * @param mixed $field + * @param string $match + * @param string $side + * @param bool $escape + * @return CI_DB_query_builder + */ + public function or_not_like($field, $match = '', $side = 'both', $escape = NULL) + { + return $this->_like($field, $match, 'OR ', $side, 'NOT', $escape); + } + + // -------------------------------------------------------------------- + + /** + * Internal LIKE + * + * @used-by like() + * @used-by or_like() + * @used-by not_like() + * @used-by or_not_like() + * + * @param mixed $field + * @param string $match + * @param string $type + * @param string $side + * @param string $not + * @param bool $escape + * @return CI_DB_query_builder + */ + protected function _like($field, $match = '', $type = 'AND ', $side = 'both', $not = '', $escape = NULL) + { + if ( ! is_array($field)) + { + $field = array($field => $match); + } + + is_bool($escape) OR $escape = $this->_protect_identifiers; + // lowercase $side in case somebody writes e.g. 'BEFORE' instead of 'before' (doh) + $side = strtolower($side); + + foreach ($field as $k => $v) + { + $prefix = (count($this->qb_where) === 0 && count($this->qb_cache_where) === 0) + ? $this->_group_get_type('') : $this->_group_get_type($type); + + if ($escape === TRUE) + { + $v = $this->escape_like_str($v); + } + + if ($side === 'none') + { + $like_statement = "{$prefix} {$k} {$not} LIKE '{$v}'"; + } + elseif ($side === 'before') + { + $like_statement = "{$prefix} {$k} {$not} LIKE '%{$v}'"; + } + elseif ($side === 'after') + { + $like_statement = "{$prefix} {$k} {$not} LIKE '{$v}%'"; + } + else + { + $like_statement = "{$prefix} {$k} {$not} LIKE '%{$v}%'"; + } + + // some platforms require an escape sequence definition for LIKE wildcards + if ($escape === TRUE && $this->_like_escape_str !== '') + { + $like_statement .= sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + $this->qb_where[] = array('condition' => $like_statement, 'escape' => $escape); + if ($this->qb_caching === TRUE) + { + $this->qb_cache_where[] = array('condition' => $like_statement, 'escape' => $escape); + $this->qb_cache_exists[] = 'where'; + } + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Starts a query group. + * + * @param string $not (Internal use only) + * @param string $type (Internal use only) + * @return CI_DB_query_builder + */ + public function group_start($not = '', $type = 'AND ') + { + $type = $this->_group_get_type($type); + + $this->qb_where_group_started = TRUE; + $prefix = (count($this->qb_where) === 0 && count($this->qb_cache_where) === 0) ? '' : $type; + $where = array( + 'condition' => $prefix.$not.str_repeat(' ', ++$this->qb_where_group_count).' (', + 'escape' => FALSE + ); + + $this->qb_where[] = $where; + if ($this->qb_caching) + { + $this->qb_cache_where[] = $where; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Starts a query group, but ORs the group + * + * @return CI_DB_query_builder + */ + public function or_group_start() + { + return $this->group_start('', 'OR '); + } + + // -------------------------------------------------------------------- + + /** + * Starts a query group, but NOTs the group + * + * @return CI_DB_query_builder + */ + public function not_group_start() + { + return $this->group_start('NOT ', 'AND '); + } + + // -------------------------------------------------------------------- + + /** + * Starts a query group, but OR NOTs the group + * + * @return CI_DB_query_builder + */ + public function or_not_group_start() + { + return $this->group_start('NOT ', 'OR '); + } + + // -------------------------------------------------------------------- + + /** + * Ends a query group + * + * @return CI_DB_query_builder + */ + public function group_end() + { + $this->qb_where_group_started = FALSE; + $where = array( + 'condition' => str_repeat(' ', $this->qb_where_group_count--).')', + 'escape' => FALSE + ); + + $this->qb_where[] = $where; + if ($this->qb_caching) + { + $this->qb_cache_where[] = $where; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Group_get_type + * + * @used-by group_start() + * @used-by _like() + * @used-by _wh() + * @used-by _where_in() + * + * @param string $type + * @return string + */ + protected function _group_get_type($type) + { + if ($this->qb_where_group_started) + { + $type = ''; + $this->qb_where_group_started = FALSE; + } + + return $type; + } + + // -------------------------------------------------------------------- + + /** + * GROUP BY + * + * @param string $by + * @param bool $escape + * @return CI_DB_query_builder + */ + public function group_by($by, $escape = NULL) + { + is_bool($escape) OR $escape = $this->_protect_identifiers; + + if (is_string($by)) + { + $by = ($escape === TRUE) + ? explode(',', $by) + : array($by); + } + + foreach ($by as $val) + { + $val = trim($val); + + if ($val !== '') + { + $val = array('field' => $val, 'escape' => $escape); + + $this->qb_groupby[] = $val; + if ($this->qb_caching === TRUE) + { + $this->qb_cache_groupby[] = $val; + $this->qb_cache_exists[] = 'groupby'; + } + } + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * HAVING + * + * Separates multiple calls with 'AND'. + * + * @param string $key + * @param string $value + * @param bool $escape + * @return CI_DB_query_builder + */ + public function having($key, $value = NULL, $escape = NULL) + { + return $this->_wh('qb_having', $key, $value, 'AND ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * OR HAVING + * + * Separates multiple calls with 'OR'. + * + * @param string $key + * @param string $value + * @param bool $escape + * @return CI_DB_query_builder + */ + public function or_having($key, $value = NULL, $escape = NULL) + { + return $this->_wh('qb_having', $key, $value, 'OR ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * ORDER BY + * + * @param string $orderby + * @param string $direction ASC, DESC or RANDOM + * @param bool $escape + * @return CI_DB_query_builder + */ + public function order_by($orderby, $direction = '', $escape = NULL) + { + $direction = strtoupper(trim($direction)); + + if ($direction === 'RANDOM') + { + $direction = ''; + + // Do we have a seed value? + $orderby = ctype_digit((string) $orderby) + ? sprintf($this->_random_keyword[1], $orderby) + : $this->_random_keyword[0]; + } + elseif (empty($orderby)) + { + return $this; + } + elseif ($direction !== '') + { + $direction = in_array($direction, array('ASC', 'DESC'), TRUE) ? ' '.$direction : ''; + } + + is_bool($escape) OR $escape = $this->_protect_identifiers; + + if ($escape === FALSE) + { + $qb_orderby[] = array('field' => $orderby, 'direction' => $direction, 'escape' => FALSE); + } + else + { + $qb_orderby = array(); + foreach (explode(',', $orderby) as $field) + { + $qb_orderby[] = ($direction === '' && preg_match('/\s+(ASC|DESC)$/i', rtrim($field), $match, PREG_OFFSET_CAPTURE)) + ? array('field' => ltrim(substr($field, 0, $match[0][1])), 'direction' => ' '.$match[1][0], 'escape' => TRUE) + : array('field' => trim($field), 'direction' => $direction, 'escape' => TRUE); + } + } + + $this->qb_orderby = array_merge($this->qb_orderby, $qb_orderby); + if ($this->qb_caching === TRUE) + { + $this->qb_cache_orderby = array_merge($this->qb_cache_orderby, $qb_orderby); + $this->qb_cache_exists[] = 'orderby'; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * @param int $value LIMIT value + * @param int $offset OFFSET value + * @return CI_DB_query_builder + */ + public function limit($value, $offset = 0) + { + is_null($value) OR $this->qb_limit = (int) $value; + empty($offset) OR $this->qb_offset = (int) $offset; + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Sets the OFFSET value + * + * @param int $offset OFFSET value + * @return CI_DB_query_builder + */ + public function offset($offset) + { + empty($offset) OR $this->qb_offset = (int) $offset; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * LIMIT string + * + * Generates a platform-specific LIMIT clause. + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + return $sql.' LIMIT '.($this->qb_offset ? $this->qb_offset.', ' : '').(int) $this->qb_limit; + } + + // -------------------------------------------------------------------- + + /** + * The "set" function. + * + * Allows key/value pairs to be set for inserting or updating + * + * @param mixed + * @param string + * @param bool + * @return CI_DB_query_builder + */ + public function set($key, $value = '', $escape = NULL) + { + $key = $this->_object_to_array($key); + + if ( ! is_array($key)) + { + $key = array($key => $value); + } + + is_bool($escape) OR $escape = $this->_protect_identifiers; + + foreach ($key as $k => $v) + { + $this->qb_set[$this->protect_identifiers($k, FALSE, $escape)] = ($escape) + ? $this->escape($v) : $v; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Get SELECT query string + * + * Compiles a SELECT query string and returns the sql. + * + * @param string the table name to select from (optional) + * @param bool TRUE: resets QB values; FALSE: leave QB values alone + * @return string + */ + public function get_compiled_select($table = '', $reset = TRUE) + { + if ($table !== '') + { + $this->_track_aliases($table); + $this->from($table); + } + + $select = $this->_compile_select(); + + if ($reset === TRUE) + { + $this->_reset_select(); + } + + return $select; + } + + // -------------------------------------------------------------------- + + /** + * Get + * + * Compiles the select statement based on the other functions called + * and runs the query + * + * @param string the table + * @param string the limit clause + * @param string the offset clause + * @return CI_DB_result + */ + public function get($table = '', $limit = NULL, $offset = NULL) + { + if ($table !== '') + { + $this->_track_aliases($table); + $this->from($table); + } + + if ( ! empty($limit)) + { + $this->limit($limit, $offset); + } + + $result = $this->query($this->_compile_select()); + $this->_reset_select(); + return $result; + } + + // -------------------------------------------------------------------- + + /** + * "Count All Results" query + * + * Generates a platform-specific query string that counts all records + * returned by an Query Builder query. + * + * @param string + * @param bool the reset clause + * @return int + */ + public function count_all_results($table = '', $reset = TRUE) + { + if ($table !== '') + { + $this->_track_aliases($table); + $this->from($table); + } + + // ORDER BY usage is often problematic here (most notably + // on Microsoft SQL Server) and ultimately unnecessary + // for selecting COUNT(*) ... + if ( ! empty($this->qb_orderby)) + { + $orderby = $this->qb_orderby; + $this->qb_orderby = NULL; + } + + $result = ($this->qb_distinct === TRUE OR ! empty($this->qb_groupby)) + ? $this->query($this->_count_string.$this->protect_identifiers('numrows')."\nFROM (\n".$this->_compile_select()."\n) CI_count_all_results") + : $this->query($this->_compile_select($this->_count_string.$this->protect_identifiers('numrows'))); + + if ($reset === TRUE) + { + $this->_reset_select(); + } + // If we've previously reset the qb_orderby values, get them back + elseif ( ! isset($this->qb_orderby)) + { + $this->qb_orderby = $orderby; + } + + if ($result->num_rows() === 0) + { + return 0; + } + + $row = $result->row(); + return (int) $row->numrows; + } + + // -------------------------------------------------------------------- + + /** + * Get_Where + * + * Allows the where clause, limit and offset to be added directly + * + * @param string $table + * @param string $where + * @param int $limit + * @param int $offset + * @return CI_DB_result + */ + public function get_where($table = '', $where = NULL, $limit = NULL, $offset = NULL) + { + if ($table !== '') + { + $this->from($table); + } + + if ($where !== NULL) + { + $this->where($where); + } + + if ( ! empty($limit)) + { + $this->limit($limit, $offset); + } + + $result = $this->query($this->_compile_select()); + $this->_reset_select(); + return $result; + } + + // -------------------------------------------------------------------- + + /** + * Insert_Batch + * + * Compiles batch insert strings and runs the queries + * + * @param string $table Table to insert into + * @param array $set An associative array of insert values + * @param bool $escape Whether to escape values and identifiers + * @return int Number of rows inserted or FALSE on failure + */ + public function insert_batch($table, $set = NULL, $escape = NULL, $batch_size = 100) + { + if ($set === NULL) + { + if (empty($this->qb_set)) + { + return ($this->db_debug) ? $this->display_error('db_must_use_set') : FALSE; + } + } + else + { + if (empty($set)) + { + return ($this->db_debug) ? $this->display_error('insert_batch() called with no data') : FALSE; + } + + $this->set_insert_batch($set, '', $escape); + } + + if (strlen($table) === 0) + { + if ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + $table = $this->qb_from[0]; + } + + // Batch this baby + $affected_rows = 0; + for ($i = 0, $total = count($this->qb_set); $i < $total; $i += $batch_size) + { + if ($this->query($this->_insert_batch($this->protect_identifiers($table, TRUE, $escape, FALSE), $this->qb_keys, array_slice($this->qb_set, $i, $batch_size)))) + { + $affected_rows += $this->affected_rows(); + } + } + + $this->_reset_write(); + return $affected_rows; + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string + */ + protected function _insert_batch($table, $keys, $values) + { + return 'INSERT INTO '.$table.' ('.implode(', ', $keys).') VALUES '.implode(', ', $values); + } + + // -------------------------------------------------------------------- + + /** + * The "set_insert_batch" function. Allows key/value pairs to be set for batch inserts + * + * @param mixed + * @param string + * @param bool + * @return CI_DB_query_builder + */ + public function set_insert_batch($key, $value = '', $escape = NULL) + { + $key = $this->_object_to_array_batch($key); + + if ( ! is_array($key)) + { + $key = array($key => $value); + } + + is_bool($escape) OR $escape = $this->_protect_identifiers; + + $keys = array_keys($this->_object_to_array(current($key))); + sort($keys); + + foreach ($key as $row) + { + $row = $this->_object_to_array($row); + if (count(array_diff($keys, array_keys($row))) > 0 OR count(array_diff(array_keys($row), $keys)) > 0) + { + // batch function above returns an error on an empty array + $this->qb_set[] = array(); + return; + } + + ksort($row); // puts $row in the same order as our keys + + if ($escape !== FALSE) + { + $clean = array(); + foreach ($row as $value) + { + $clean[] = $this->escape($value); + } + + $row = $clean; + } + + $this->qb_set[] = '('.implode(',', $row).')'; + } + + foreach ($keys as $k) + { + $this->qb_keys[] = $this->protect_identifiers($k, FALSE, $escape); + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Get INSERT query string + * + * Compiles an insert query and returns the sql + * + * @param string the table to insert into + * @param bool TRUE: reset QB values; FALSE: leave QB values alone + * @return string + */ + public function get_compiled_insert($table = '', $reset = TRUE) + { + if ($this->_validate_insert($table) === FALSE) + { + return FALSE; + } + + $sql = $this->_insert( + $this->protect_identifiers( + $this->qb_from[0], TRUE, NULL, FALSE + ), + array_keys($this->qb_set), + array_values($this->qb_set) + ); + + if ($reset === TRUE) + { + $this->_reset_write(); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Insert + * + * Compiles an insert string and runs the query + * + * @param string the table to insert data into + * @param array an associative array of insert values + * @param bool $escape Whether to escape values and identifiers + * @return bool TRUE on success, FALSE on failure + */ + public function insert($table = '', $set = NULL, $escape = NULL) + { + if ($set !== NULL) + { + $this->set($set, '', $escape); + } + + if ($this->_validate_insert($table) === FALSE) + { + return FALSE; + } + + $sql = $this->_insert( + $this->protect_identifiers( + $this->qb_from[0], TRUE, $escape, FALSE + ), + array_keys($this->qb_set), + array_values($this->qb_set) + ); + + $this->_reset_write(); + return $this->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Validate Insert + * + * This method is used by both insert() and get_compiled_insert() to + * validate that the there data is actually being set and that table + * has been chosen to be inserted into. + * + * @param string the table to insert data into + * @return string + */ + protected function _validate_insert($table = '') + { + if (count($this->qb_set) === 0) + { + return ($this->db_debug) ? $this->display_error('db_must_use_set') : FALSE; + } + + if ($table !== '') + { + $this->qb_from[0] = $table; + } + elseif ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Replace + * + * Compiles an replace into string and runs the query + * + * @param string the table to replace data into + * @param array an associative array of insert values + * @return bool TRUE on success, FALSE on failure + */ + public function replace($table = '', $set = NULL) + { + if ($set !== NULL) + { + $this->set($set); + } + + if (count($this->qb_set) === 0) + { + return ($this->db_debug) ? $this->display_error('db_must_use_set') : FALSE; + } + + if ($table === '') + { + if ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + $table = $this->qb_from[0]; + } + + $sql = $this->_replace($this->protect_identifiers($table, TRUE, NULL, FALSE), array_keys($this->qb_set), array_values($this->qb_set)); + + $this->_reset_write(); + return $this->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Replace statement + * + * Generates a platform-specific replace string from the supplied data + * + * @param string the table name + * @param array the insert keys + * @param array the insert values + * @return string + */ + protected function _replace($table, $keys, $values) + { + return 'REPLACE INTO '.$table.' ('.implode(', ', $keys).') VALUES ('.implode(', ', $values).')'; + } + + // -------------------------------------------------------------------- + + /** + * FROM tables + * + * Groups tables in FROM clauses if needed, so there is no confusion + * about operator precedence. + * + * Note: This is only used (and overridden) by MySQL and CUBRID. + * + * @return string + */ + protected function _from_tables() + { + return implode(', ', $this->qb_from); + } + + // -------------------------------------------------------------------- + + /** + * Get UPDATE query string + * + * Compiles an update query and returns the sql + * + * @param string the table to update + * @param bool TRUE: reset QB values; FALSE: leave QB values alone + * @return string + */ + public function get_compiled_update($table = '', $reset = TRUE) + { + // Combine any cached components with the current statements + $this->_merge_cache(); + + if ($this->_validate_update($table) === FALSE) + { + return FALSE; + } + + $sql = $this->_update($this->qb_from[0], $this->qb_set); + + if ($reset === TRUE) + { + $this->_reset_write(); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * UPDATE + * + * Compiles an update string and runs the query. + * + * @param string $table + * @param array $set An associative array of update values + * @param mixed $where + * @param int $limit + * @return bool TRUE on success, FALSE on failure + */ + public function update($table = '', $set = NULL, $where = NULL, $limit = NULL) + { + // Combine any cached components with the current statements + $this->_merge_cache(); + + if ($set !== NULL) + { + $this->set($set); + } + + if ($this->_validate_update($table) === FALSE) + { + return FALSE; + } + + if ($where !== NULL) + { + $this->where($where); + } + + if ( ! empty($limit)) + { + $this->limit($limit); + } + + $sql = $this->_update($this->qb_from[0], $this->qb_set); + $this->_reset_write(); + return $this->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Validate Update + * + * This method is used by both update() and get_compiled_update() to + * validate that data is actually being set and that a table has been + * chosen to be update. + * + * @param string the table to update data on + * @return bool + */ + protected function _validate_update($table) + { + if (count($this->qb_set) === 0) + { + return ($this->db_debug) ? $this->display_error('db_must_use_set') : FALSE; + } + + if ($table !== '') + { + $this->qb_from = array($this->protect_identifiers($table, TRUE, NULL, FALSE)); + } + elseif ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Update_Batch + * + * Compiles an update string and runs the query + * + * @param string the table to retrieve the results from + * @param array an associative array of update values + * @param string the where key + * @return int number of rows affected or FALSE on failure + */ + public function update_batch($table, $set = NULL, $index = NULL, $batch_size = 100) + { + // Combine any cached components with the current statements + $this->_merge_cache(); + + if ($index === NULL) + { + return ($this->db_debug) ? $this->display_error('db_must_use_index') : FALSE; + } + + if ($set === NULL) + { + if (empty($this->qb_set)) + { + return ($this->db_debug) ? $this->display_error('db_must_use_set') : FALSE; + } + } + else + { + if (empty($set)) + { + return ($this->db_debug) ? $this->display_error('update_batch() called with no data') : FALSE; + } + + $this->set_update_batch($set, $index); + } + + if (strlen($table) === 0) + { + if ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + $table = $this->qb_from[0]; + } + + // Batch this baby + $affected_rows = 0; + for ($i = 0, $total = count($this->qb_set); $i < $total; $i += $batch_size) + { + if ($this->query($this->_update_batch($this->protect_identifiers($table, TRUE, NULL, FALSE), array_slice($this->qb_set, $i, $batch_size), $index))) + { + $affected_rows += $this->affected_rows(); + } + + $this->qb_where = array(); + } + + $this->_reset_write(); + return $affected_rows; + } + + // -------------------------------------------------------------------- + + /** + * Update_Batch statement + * + * Generates a platform-specific batch update string from the supplied data + * + * @param string $table Table name + * @param array $values Update data + * @param string $index WHERE key + * @return string + */ + protected function _update_batch($table, $values, $index) + { + $index_escaped = $this->protect_identifiers($index); + + $ids = array(); + foreach ($values as $key => $val) + { + $ids[] = $val[$index]; + + foreach (array_keys($val) as $field) + { + if ($field !== $index) + { + $final[$field][] = 'WHEN '.$index_escaped.' = '.$val[$index].' THEN '.$val[$field]; + } + } + } + + $cases = ''; + foreach ($final as $k => $v) + { + $cases .= $k." = CASE \n" + .implode("\n", $v)."\n" + .'ELSE '.$k.' END, '; + } + + $this->where($index_escaped.' IN('.implode(',', $ids).')', NULL, FALSE); + + return 'UPDATE '.$table.' SET '.substr($cases, 0, -2).$this->_compile_wh('qb_where'); + } + + // -------------------------------------------------------------------- + + /** + * The "set_update_batch" function. Allows key/value pairs to be set for batch updating + * + * @param array + * @param string + * @param bool + * @return CI_DB_query_builder + */ + public function set_update_batch($key, $index = '', $escape = NULL) + { + $key = $this->_object_to_array_batch($key); + + if ( ! is_array($key)) + { + // @todo error + } + + is_bool($escape) OR $escape = $this->_protect_identifiers; + + foreach ($key as $k => $v) + { + $index_set = FALSE; + $clean = array(); + foreach ($v as $k2 => $v2) + { + if ($k2 === $index) + { + $index_set = TRUE; + } + + $clean[$this->protect_identifiers($k2, FALSE, $escape)] = ($escape === FALSE) ? $v2 : $this->escape($v2); + } + + if ($index_set === FALSE) + { + return $this->display_error('db_batch_missing_index'); + } + + $this->qb_set[] = $clean; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Empty Table + * + * Compiles a delete string and runs "DELETE FROM table" + * + * @param string the table to empty + * @return bool TRUE on success, FALSE on failure + */ + public function empty_table($table = '') + { + if ($table === '') + { + if ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + $table = $this->qb_from[0]; + } + else + { + $table = $this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + $sql = $this->_delete($table); + $this->_reset_write(); + return $this->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Truncate + * + * Compiles a truncate string and runs the query + * If the database does not support the truncate() command + * This function maps to "DELETE FROM table" + * + * @param string the table to truncate + * @return bool TRUE on success, FALSE on failure + */ + public function truncate($table = '') + { + if ($table === '') + { + if ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + $table = $this->qb_from[0]; + } + else + { + $table = $this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + $sql = $this->_truncate($table); + $this->_reset_write(); + return $this->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the truncate() command, + * then this method maps to 'DELETE FROM table' + * + * @param string the table name + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Get DELETE query string + * + * Compiles a delete query string and returns the sql + * + * @param string the table to delete from + * @param bool TRUE: reset QB values; FALSE: leave QB values alone + * @return string + */ + public function get_compiled_delete($table = '', $reset = TRUE) + { + $this->return_delete_sql = TRUE; + $sql = $this->delete($table, '', NULL, $reset); + $this->return_delete_sql = FALSE; + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Delete + * + * Compiles a delete string and runs the query + * + * @param mixed the table(s) to delete from. String or array + * @param mixed the where clause + * @param mixed the limit clause + * @param bool + * @return mixed + */ + public function delete($table = '', $where = '', $limit = NULL, $reset_data = TRUE) + { + // Combine any cached components with the current statements + $this->_merge_cache(); + + if ($table === '') + { + if ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + $table = $this->qb_from[0]; + } + elseif (is_array($table)) + { + empty($where) && $reset_data = FALSE; + + foreach ($table as $single_table) + { + $this->delete($single_table, $where, $limit, $reset_data); + } + + return; + } + else + { + $table = $this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + if ($where !== '') + { + $this->where($where); + } + + if ( ! empty($limit)) + { + $this->limit($limit); + } + + if (count($this->qb_where) === 0) + { + return ($this->db_debug) ? $this->display_error('db_del_must_use_where') : FALSE; + } + + $sql = $this->_delete($table); + if ($reset_data) + { + $this->_reset_write(); + } + + return ($this->return_delete_sql === TRUE) ? $sql : $this->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string the table name + * @return string + */ + protected function _delete($table) + { + return 'DELETE FROM '.$table.$this->_compile_wh('qb_where') + .($this->qb_limit ? ' LIMIT '.$this->qb_limit : ''); + } + + // -------------------------------------------------------------------- + + /** + * DB Prefix + * + * Prepends a database prefix if one exists in configuration + * + * @param string the table + * @return string + */ + public function dbprefix($table = '') + { + if ($table === '') + { + $this->display_error('db_table_name_required'); + } + + return $this->dbprefix.$table; + } + + // -------------------------------------------------------------------- + + /** + * Set DB Prefix + * + * Set's the DB Prefix to something new without needing to reconnect + * + * @param string the prefix + * @return string + */ + public function set_dbprefix($prefix = '') + { + return $this->dbprefix = $prefix; + } + + // -------------------------------------------------------------------- + + /** + * Track Aliases + * + * Used to track SQL statements written with aliased tables. + * + * @param string The table to inspect + * @return string + */ + protected function _track_aliases($table) + { + if (is_array($table)) + { + foreach ($table as $t) + { + $this->_track_aliases($t); + } + return; + } + + // Does the string contain a comma? If so, we need to separate + // the string into discreet statements + if (strpos($table, ',') !== FALSE) + { + return $this->_track_aliases(explode(',', $table)); + } + + // if a table alias is used we can recognize it by a space + if (strpos($table, ' ') !== FALSE) + { + // if the alias is written with the AS keyword, remove it + $table = preg_replace('/\s+AS\s+/i', ' ', $table); + + // Grab the alias + $table = trim(strrchr($table, ' ')); + + // Store the alias, if it doesn't already exist + if ( ! in_array($table, $this->qb_aliased_tables)) + { + $this->qb_aliased_tables[] = $table; + } + } + } + + // -------------------------------------------------------------------- + + /** + * Compile the SELECT statement + * + * Generates a query string based on which functions were used. + * Should not be called directly. + * + * @param bool $select_override + * @return string + */ + protected function _compile_select($select_override = FALSE) + { + // Combine any cached components with the current statements + $this->_merge_cache(); + + // Write the "select" portion of the query + if ($select_override !== FALSE) + { + $sql = $select_override; + } + else + { + $sql = ( ! $this->qb_distinct) ? 'SELECT ' : 'SELECT DISTINCT '; + + if (count($this->qb_select) === 0) + { + $sql .= '*'; + } + else + { + // Cycle through the "select" portion of the query and prep each column name. + // The reason we protect identifiers here rather than in the select() function + // is because until the user calls the from() function we don't know if there are aliases + foreach ($this->qb_select as $key => $val) + { + $no_escape = isset($this->qb_no_escape[$key]) ? $this->qb_no_escape[$key] : NULL; + $this->qb_select[$key] = $this->protect_identifiers($val, FALSE, $no_escape); + } + + $sql .= implode(', ', $this->qb_select); + } + } + + // Write the "FROM" portion of the query + if (count($this->qb_from) > 0) + { + $sql .= "\nFROM ".$this->_from_tables(); + } + + // Write the "JOIN" portion of the query + if (count($this->qb_join) > 0) + { + $sql .= "\n".implode("\n", $this->qb_join); + } + + $sql .= $this->_compile_wh('qb_where') + .$this->_compile_group_by() + .$this->_compile_wh('qb_having') + .$this->_compile_order_by(); // ORDER BY + + // LIMIT + if ($this->qb_limit OR $this->qb_offset) + { + return $this->_limit($sql."\n"); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Compile WHERE, HAVING statements + * + * Escapes identifiers in WHERE and HAVING statements at execution time. + * + * Required so that aliases are tracked properly, regardless of whether + * where(), or_where(), having(), or_having are called prior to from(), + * join() and dbprefix is added only if needed. + * + * @param string $qb_key 'qb_where' or 'qb_having' + * @return string SQL statement + */ + protected function _compile_wh($qb_key) + { + if (count($this->$qb_key) > 0) + { + for ($i = 0, $c = count($this->$qb_key); $i < $c; $i++) + { + // Is this condition already compiled? + if (is_string($this->{$qb_key}[$i])) + { + continue; + } + elseif ($this->{$qb_key}[$i]['escape'] === FALSE) + { + $this->{$qb_key}[$i] = $this->{$qb_key}[$i]['condition']; + continue; + } + + // Split multiple conditions + $conditions = preg_split( + '/((?:^|\s+)AND\s+|(?:^|\s+)OR\s+)/i', + $this->{$qb_key}[$i]['condition'], + -1, + PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY + ); + + for ($ci = 0, $cc = count($conditions); $ci < $cc; $ci++) + { + if (($op = $this->_get_operator($conditions[$ci])) === FALSE + OR ! preg_match('/^(\(?)(.*)('.preg_quote($op, '/').')\s*(.*(? '(test <= foo)', /* the whole thing */ + // 1 => '(', /* optional */ + // 2 => 'test', /* the field name */ + // 3 => ' <= ', /* $op */ + // 4 => 'foo', /* optional, if $op is e.g. 'IS NULL' */ + // 5 => ')' /* optional */ + // ); + + if ( ! empty($matches[4])) + { + $this->_is_literal($matches[4]) OR $matches[4] = $this->protect_identifiers(trim($matches[4])); + $matches[4] = ' '.$matches[4]; + } + + $conditions[$ci] = $matches[1].$this->protect_identifiers(trim($matches[2])) + .' '.trim($matches[3]).$matches[4].$matches[5]; + } + + $this->{$qb_key}[$i] = implode('', $conditions); + } + + return ($qb_key === 'qb_having' ? "\nHAVING " : "\nWHERE ") + .implode("\n", $this->$qb_key); + } + + return ''; + } + + // -------------------------------------------------------------------- + + /** + * Compile GROUP BY + * + * Escapes identifiers in GROUP BY statements at execution time. + * + * Required so that aliases are tracked properly, regardless of wether + * group_by() is called prior to from(), join() and dbprefix is added + * only if needed. + * + * @return string SQL statement + */ + protected function _compile_group_by() + { + if (count($this->qb_groupby) > 0) + { + for ($i = 0, $c = count($this->qb_groupby); $i < $c; $i++) + { + // Is it already compiled? + if (is_string($this->qb_groupby[$i])) + { + continue; + } + + $this->qb_groupby[$i] = ($this->qb_groupby[$i]['escape'] === FALSE OR $this->_is_literal($this->qb_groupby[$i]['field'])) + ? $this->qb_groupby[$i]['field'] + : $this->protect_identifiers($this->qb_groupby[$i]['field']); + } + + return "\nGROUP BY ".implode(', ', $this->qb_groupby); + } + + return ''; + } + + // -------------------------------------------------------------------- + + /** + * Compile ORDER BY + * + * Escapes identifiers in ORDER BY statements at execution time. + * + * Required so that aliases are tracked properly, regardless of wether + * order_by() is called prior to from(), join() and dbprefix is added + * only if needed. + * + * @return string SQL statement + */ + protected function _compile_order_by() + { + if (is_array($this->qb_orderby) && count($this->qb_orderby) > 0) + { + for ($i = 0, $c = count($this->qb_orderby); $i < $c; $i++) + { + if ($this->qb_orderby[$i]['escape'] !== FALSE && ! $this->_is_literal($this->qb_orderby[$i]['field'])) + { + $this->qb_orderby[$i]['field'] = $this->protect_identifiers($this->qb_orderby[$i]['field']); + } + + $this->qb_orderby[$i] = $this->qb_orderby[$i]['field'].$this->qb_orderby[$i]['direction']; + } + + return $this->qb_orderby = "\nORDER BY ".implode(', ', $this->qb_orderby); + } + elseif (is_string($this->qb_orderby)) + { + return $this->qb_orderby; + } + + return ''; + } + + // -------------------------------------------------------------------- + + /** + * Object to Array + * + * Takes an object as input and converts the class variables to array key/vals + * + * @param object + * @return array + */ + protected function _object_to_array($object) + { + if ( ! is_object($object)) + { + return $object; + } + + $array = array(); + foreach (get_object_vars($object) as $key => $val) + { + // There are some built in keys we need to ignore for this conversion + if ( ! is_object($val) && ! is_array($val) && $key !== '_parent_name') + { + $array[$key] = $val; + } + } + + return $array; + } + + // -------------------------------------------------------------------- + + /** + * Object to Array + * + * Takes an object as input and converts the class variables to array key/vals + * + * @param object + * @return array + */ + protected function _object_to_array_batch($object) + { + if ( ! is_object($object)) + { + return $object; + } + + $array = array(); + $out = get_object_vars($object); + $fields = array_keys($out); + + foreach ($fields as $val) + { + // There are some built in keys we need to ignore for this conversion + if ($val !== '_parent_name') + { + $i = 0; + foreach ($out[$val] as $data) + { + $array[$i++][$val] = $data; + } + } + } + + return $array; + } + + // -------------------------------------------------------------------- + + /** + * Start Cache + * + * Starts QB caching + * + * @return CI_DB_query_builder + */ + public function start_cache() + { + $this->qb_caching = TRUE; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Stop Cache + * + * Stops QB caching + * + * @return CI_DB_query_builder + */ + public function stop_cache() + { + $this->qb_caching = FALSE; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Flush Cache + * + * Empties the QB cache + * + * @return CI_DB_query_builder + */ + public function flush_cache() + { + $this->_reset_run(array( + 'qb_cache_select' => array(), + 'qb_cache_from' => array(), + 'qb_cache_join' => array(), + 'qb_cache_where' => array(), + 'qb_cache_groupby' => array(), + 'qb_cache_having' => array(), + 'qb_cache_orderby' => array(), + 'qb_cache_set' => array(), + 'qb_cache_exists' => array(), + 'qb_cache_no_escape' => array() + )); + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Merge Cache + * + * When called, this function merges any cached QB arrays with + * locally called ones. + * + * @return void + */ + protected function _merge_cache() + { + if (count($this->qb_cache_exists) === 0) + { + return; + } + elseif (in_array('select', $this->qb_cache_exists, TRUE)) + { + $qb_no_escape = $this->qb_cache_no_escape; + } + + foreach (array_unique($this->qb_cache_exists) as $val) // select, from, etc. + { + $qb_variable = 'qb_'.$val; + $qb_cache_var = 'qb_cache_'.$val; + $qb_new = $this->$qb_cache_var; + + for ($i = 0, $c = count($this->$qb_variable); $i < $c; $i++) + { + if ( ! in_array($this->{$qb_variable}[$i], $qb_new, TRUE)) + { + $qb_new[] = $this->{$qb_variable}[$i]; + if ($val === 'select') + { + $qb_no_escape[] = $this->qb_no_escape[$i]; + } + } + } + + $this->$qb_variable = $qb_new; + if ($val === 'select') + { + $this->qb_no_escape = $qb_no_escape; + } + } + + // If we are "protecting identifiers" we need to examine the "from" + // portion of the query to determine if there are any aliases + if ($this->_protect_identifiers === TRUE && count($this->qb_cache_from) > 0) + { + $this->_track_aliases($this->qb_from); + } + } + + // -------------------------------------------------------------------- + + /** + * Is literal + * + * Determines if a string represents a literal value or a field name + * + * @param string $str + * @return bool + */ + protected function _is_literal($str) + { + $str = trim($str); + + if (empty($str) OR ctype_digit($str) OR (string) (float) $str === $str OR in_array(strtoupper($str), array('TRUE', 'FALSE'), TRUE)) + { + return TRUE; + } + + static $_str; + + if (empty($_str)) + { + $_str = ($this->_escape_char !== '"') + ? array('"', "'") : array("'"); + } + + return in_array($str[0], $_str, TRUE); + } + + // -------------------------------------------------------------------- + + /** + * Reset Query Builder values. + * + * Publicly-visible method to reset the QB values. + * + * @return CI_DB_query_builder + */ + public function reset_query() + { + $this->_reset_select(); + $this->_reset_write(); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Resets the query builder values. Called by the get() function + * + * @param array An array of fields to reset + * @return void + */ + protected function _reset_run($qb_reset_items) + { + foreach ($qb_reset_items as $item => $default_value) + { + $this->$item = $default_value; + } + } + + // -------------------------------------------------------------------- + + /** + * Resets the query builder values. Called by the get() function + * + * @return void + */ + protected function _reset_select() + { + $this->_reset_run(array( + 'qb_select' => array(), + 'qb_from' => array(), + 'qb_join' => array(), + 'qb_where' => array(), + 'qb_groupby' => array(), + 'qb_having' => array(), + 'qb_orderby' => array(), + 'qb_aliased_tables' => array(), + 'qb_no_escape' => array(), + 'qb_distinct' => FALSE, + 'qb_limit' => FALSE, + 'qb_offset' => FALSE + )); + } + + // -------------------------------------------------------------------- + + /** + * Resets the query builder "write" values. + * + * Called by the insert() update() insert_batch() update_batch() and delete() functions + * + * @return void + */ + protected function _reset_write() + { + $this->_reset_run(array( + 'qb_set' => array(), + 'qb_from' => array(), + 'qb_join' => array(), + 'qb_where' => array(), + 'qb_orderby' => array(), + 'qb_keys' => array(), + 'qb_limit' => FALSE + )); + } + +} diff --git a/system/database/DB_result.php b/system/database/DB_result.php new file mode 100644 index 0000000..4e24293 --- /dev/null +++ b/system/database/DB_result.php @@ -0,0 +1,666 @@ +conn_id = $driver_object->conn_id; + $this->result_id = $driver_object->result_id; + } + + // -------------------------------------------------------------------- + + /** + * Number of rows in the result set + * + * @return int + */ + public function num_rows() + { + if (is_int($this->num_rows)) + { + return $this->num_rows; + } + elseif (count($this->result_array) > 0) + { + return $this->num_rows = count($this->result_array); + } + elseif (count($this->result_object) > 0) + { + return $this->num_rows = count($this->result_object); + } + + return $this->num_rows = count($this->result_array()); + } + + // -------------------------------------------------------------------- + + /** + * Query result. Acts as a wrapper function for the following functions. + * + * @param string $type 'object', 'array' or a custom class name + * @return array + */ + public function result($type = 'object') + { + if ($type === 'array') + { + return $this->result_array(); + } + elseif ($type === 'object') + { + return $this->result_object(); + } + else + { + return $this->custom_result_object($type); + } + } + + // -------------------------------------------------------------------- + + /** + * Custom query result. + * + * @param string $class_name + * @return array + */ + public function custom_result_object($class_name) + { + if (isset($this->custom_result_object[$class_name])) + { + return $this->custom_result_object[$class_name]; + } + elseif ( ! $this->result_id OR $this->num_rows === 0) + { + return array(); + } + + // Don't fetch the result set again if we already have it + $_data = NULL; + if (($c = count($this->result_array)) > 0) + { + $_data = 'result_array'; + } + elseif (($c = count($this->result_object)) > 0) + { + $_data = 'result_object'; + } + + if ($_data !== NULL) + { + for ($i = 0; $i < $c; $i++) + { + $this->custom_result_object[$class_name][$i] = new $class_name(); + + foreach ($this->{$_data}[$i] as $key => $value) + { + $this->custom_result_object[$class_name][$i]->$key = $value; + } + } + + return $this->custom_result_object[$class_name]; + } + + is_null($this->row_data) OR $this->data_seek(0); + $this->custom_result_object[$class_name] = array(); + + while ($row = $this->_fetch_object($class_name)) + { + $this->custom_result_object[$class_name][] = $row; + } + + return $this->custom_result_object[$class_name]; + } + + // -------------------------------------------------------------------- + + /** + * Query result. "object" version. + * + * @return array + */ + public function result_object() + { + if (count($this->result_object) > 0) + { + return $this->result_object; + } + + // In the event that query caching is on, the result_id variable + // will not be a valid resource so we'll simply return an empty + // array. + if ( ! $this->result_id OR $this->num_rows === 0) + { + return array(); + } + + if (($c = count($this->result_array)) > 0) + { + for ($i = 0; $i < $c; $i++) + { + $this->result_object[$i] = (object) $this->result_array[$i]; + } + + return $this->result_object; + } + + is_null($this->row_data) OR $this->data_seek(0); + while ($row = $this->_fetch_object()) + { + $this->result_object[] = $row; + } + + return $this->result_object; + } + + // -------------------------------------------------------------------- + + /** + * Query result. "array" version. + * + * @return array + */ + public function result_array() + { + if (count($this->result_array) > 0) + { + return $this->result_array; + } + + // In the event that query caching is on, the result_id variable + // will not be a valid resource so we'll simply return an empty + // array. + if ( ! $this->result_id OR $this->num_rows === 0) + { + return array(); + } + + if (($c = count($this->result_object)) > 0) + { + for ($i = 0; $i < $c; $i++) + { + $this->result_array[$i] = (array) $this->result_object[$i]; + } + + return $this->result_array; + } + + is_null($this->row_data) OR $this->data_seek(0); + while ($row = $this->_fetch_assoc()) + { + $this->result_array[] = $row; + } + + return $this->result_array; + } + + // -------------------------------------------------------------------- + + /** + * Row + * + * A wrapper method. + * + * @param mixed $n + * @param string $type 'object' or 'array' + * @return mixed + */ + public function row($n = 0, $type = 'object') + { + if ( ! is_numeric($n)) + { + // We cache the row data for subsequent uses + is_array($this->row_data) OR $this->row_data = $this->row_array(0); + + // array_key_exists() instead of isset() to allow for NULL values + if (empty($this->row_data) OR ! array_key_exists($n, $this->row_data)) + { + return NULL; + } + + return $this->row_data[$n]; + } + + if ($type === 'object') return $this->row_object($n); + elseif ($type === 'array') return $this->row_array($n); + else return $this->custom_row_object($n, $type); + } + + // -------------------------------------------------------------------- + + /** + * Assigns an item into a particular column slot + * + * @param mixed $key + * @param mixed $value + * @return void + */ + public function set_row($key, $value = NULL) + { + // We cache the row data for subsequent uses + if ( ! is_array($this->row_data)) + { + $this->row_data = $this->row_array(0); + } + + if (is_array($key)) + { + foreach ($key as $k => $v) + { + $this->row_data[$k] = $v; + } + return; + } + + if ($key !== '' && $value !== NULL) + { + $this->row_data[$key] = $value; + } + } + + // -------------------------------------------------------------------- + + /** + * Returns a single result row - custom object version + * + * @param int $n + * @param string $type + * @return object + */ + public function custom_row_object($n, $type) + { + isset($this->custom_result_object[$type]) OR $this->custom_result_object($type); + + if (count($this->custom_result_object[$type]) === 0) + { + return NULL; + } + + if ($n !== $this->current_row && isset($this->custom_result_object[$type][$n])) + { + $this->current_row = $n; + } + + return $this->custom_result_object[$type][$this->current_row]; + } + + // -------------------------------------------------------------------- + + /** + * Returns a single result row - object version + * + * @param int $n + * @return object + */ + public function row_object($n = 0) + { + $result = $this->result_object(); + if (count($result) === 0) + { + return NULL; + } + + if ($n !== $this->current_row && isset($result[$n])) + { + $this->current_row = $n; + } + + return $result[$this->current_row]; + } + + // -------------------------------------------------------------------- + + /** + * Returns a single result row - array version + * + * @param int $n + * @return array + */ + public function row_array($n = 0) + { + $result = $this->result_array(); + if (count($result) === 0) + { + return NULL; + } + + if ($n !== $this->current_row && isset($result[$n])) + { + $this->current_row = $n; + } + + return $result[$this->current_row]; + } + + // -------------------------------------------------------------------- + + /** + * Returns the "first" row + * + * @param string $type + * @return mixed + */ + public function first_row($type = 'object') + { + $result = $this->result($type); + return (count($result) === 0) ? NULL : $result[0]; + } + + // -------------------------------------------------------------------- + + /** + * Returns the "last" row + * + * @param string $type + * @return mixed + */ + public function last_row($type = 'object') + { + $result = $this->result($type); + return (count($result) === 0) ? NULL : $result[count($result) - 1]; + } + + // -------------------------------------------------------------------- + + /** + * Returns the "next" row + * + * @param string $type + * @return mixed + */ + public function next_row($type = 'object') + { + $result = $this->result($type); + if (count($result) === 0) + { + return NULL; + } + + return isset($result[$this->current_row + 1]) + ? $result[++$this->current_row] + : NULL; + } + + // -------------------------------------------------------------------- + + /** + * Returns the "previous" row + * + * @param string $type + * @return mixed + */ + public function previous_row($type = 'object') + { + $result = $this->result($type); + if (count($result) === 0) + { + return NULL; + } + + if (isset($result[$this->current_row - 1])) + { + --$this->current_row; + } + return $result[$this->current_row]; + } + + // -------------------------------------------------------------------- + + /** + * Returns an unbuffered row and move pointer to next row + * + * @param string $type 'array', 'object' or a custom class name + * @return mixed + */ + public function unbuffered_row($type = 'object') + { + if ($type === 'array') + { + return $this->_fetch_assoc(); + } + elseif ($type === 'object') + { + return $this->_fetch_object(); + } + + return $this->_fetch_object($type); + } + + // -------------------------------------------------------------------- + + /** + * The following methods are normally overloaded by the identically named + * methods in the platform-specific driver -- except when query caching + * is used. When caching is enabled we do not load the other driver. + * These functions are primarily here to prevent undefined function errors + * when a cached result object is in use. They are not otherwise fully + * operational due to the unavailability of the database resource IDs with + * cached results. + */ + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * Overridden by driver result classes. + * + * @return int + */ + public function num_fields() + { + return 0; + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names. + * + * Overridden by driver result classes. + * + * @return array + */ + public function list_fields() + { + return array(); + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data. + * + * Overridden by driver result classes. + * + * @return array + */ + public function field_data() + { + return array(); + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * Overridden by driver result classes. + * + * @return void + */ + public function free_result() + { + $this->result_id = FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * Overridden by driver result classes. + * + * @param int $n + * @return bool + */ + public function data_seek($n = 0) + { + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array. + * + * Overridden by driver result classes. + * + * @return array + */ + protected function _fetch_assoc() + { + return array(); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object. + * + * Overridden by driver result classes. + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return new $class_name(); + } + +} diff --git a/system/database/DB_utility.php b/system/database/DB_utility.php new file mode 100644 index 0000000..7052828 --- /dev/null +++ b/system/database/DB_utility.php @@ -0,0 +1,424 @@ +db =& $db; + log_message('info', 'Database Utility Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * List databases + * + * @return array + */ + public function list_databases() + { + // Is there a cached result? + if (isset($this->db->data_cache['db_names'])) + { + return $this->db->data_cache['db_names']; + } + elseif ($this->_list_databases === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + $this->db->data_cache['db_names'] = array(); + + $query = $this->db->query($this->_list_databases); + if ($query === FALSE) + { + return $this->db->data_cache['db_names']; + } + + for ($i = 0, $query = $query->result_array(), $c = count($query); $i < $c; $i++) + { + $this->db->data_cache['db_names'][] = current($query[$i]); + } + + return $this->db->data_cache['db_names']; + } + + // -------------------------------------------------------------------- + + /** + * Determine if a particular database exists + * + * @param string $database_name + * @return bool + */ + public function database_exists($database_name) + { + return in_array($database_name, $this->list_databases()); + } + + // -------------------------------------------------------------------- + + /** + * Optimize Table + * + * @param string $table_name + * @return mixed + */ + public function optimize_table($table_name) + { + if ($this->_optimize_table === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + $query = $this->db->query(sprintf($this->_optimize_table, $this->db->escape_identifiers($table_name))); + if ($query !== FALSE) + { + $query = $query->result_array(); + return current($query); + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Optimize Database + * + * @return mixed + */ + public function optimize_database() + { + if ($this->_optimize_table === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + $result = array(); + foreach ($this->db->list_tables() as $table_name) + { + $res = $this->db->query(sprintf($this->_optimize_table, $this->db->escape_identifiers($table_name))); + if (is_bool($res)) + { + return $res; + } + + // Build the result array... + $res = $res->result_array(); + $res = current($res); + $key = str_replace($this->db->database.'.', '', current($res)); + $keys = array_keys($res); + unset($res[$keys[0]]); + + $result[$key] = $res; + } + + return $result; + } + + // -------------------------------------------------------------------- + + /** + * Repair Table + * + * @param string $table_name + * @return mixed + */ + public function repair_table($table_name) + { + if ($this->_repair_table === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + $query = $this->db->query(sprintf($this->_repair_table, $this->db->escape_identifiers($table_name))); + if (is_bool($query)) + { + return $query; + } + + $query = $query->result_array(); + return current($query); + } + + // -------------------------------------------------------------------- + + /** + * Generate CSV from a query result object + * + * @param object $query Query result object + * @param string $delim Delimiter (default: ,) + * @param string $newline Newline character (default: \n) + * @param string $enclosure Enclosure (default: ") + * @return string + */ + public function csv_from_result($query, $delim = ',', $newline = "\n", $enclosure = '"') + { + if ( ! is_object($query) OR ! method_exists($query, 'list_fields')) + { + show_error('You must submit a valid result object'); + } + + $out = ''; + // First generate the headings from the table column names + foreach ($query->list_fields() as $name) + { + $out .= $enclosure.str_replace($enclosure, $enclosure.$enclosure, $name).$enclosure.$delim; + } + + $out = substr($out, 0, -strlen($delim)).$newline; + + // Next blast through the result array and build out the rows + while ($row = $query->unbuffered_row('array')) + { + $line = array(); + foreach ($row as $item) + { + $line[] = $enclosure.str_replace($enclosure, $enclosure.$enclosure, $item).$enclosure; + } + $out .= implode($delim, $line).$newline; + } + + return $out; + } + + // -------------------------------------------------------------------- + + /** + * Generate XML data from a query result object + * + * @param object $query Query result object + * @param array $params Any preferences + * @return string + */ + public function xml_from_result($query, $params = array()) + { + if ( ! is_object($query) OR ! method_exists($query, 'list_fields')) + { + show_error('You must submit a valid result object'); + } + + // Set our default values + foreach (array('root' => 'root', 'element' => 'element', 'newline' => "\n", 'tab' => "\t") as $key => $val) + { + if ( ! isset($params[$key])) + { + $params[$key] = $val; + } + } + + // Create variables for convenience + extract($params); + + // Load the xml helper + get_instance()->load->helper('xml'); + + // Generate the result + $xml = '<'.$root.'>'.$newline; + while ($row = $query->unbuffered_row()) + { + $xml .= $tab.'<'.$element.'>'.$newline; + foreach ($row as $key => $val) + { + $xml .= $tab.$tab.'<'.$key.'>'.xml_convert($val).''.$newline; + } + $xml .= $tab.''.$newline; + } + + return $xml.''.$newline; + } + + // -------------------------------------------------------------------- + + /** + * Database Backup + * + * @param array $params + * @return string + */ + public function backup($params = array()) + { + // If the parameters have not been submitted as an + // array then we know that it is simply the table + // name, which is a valid short cut. + if (is_string($params)) + { + $params = array('tables' => $params); + } + + // Set up our default preferences + $prefs = array( + 'tables' => array(), + 'ignore' => array(), + 'filename' => '', + 'format' => 'gzip', // gzip, zip, txt + 'add_drop' => TRUE, + 'add_insert' => TRUE, + 'newline' => "\n", + 'foreign_key_checks' => TRUE + ); + + // Did the user submit any preferences? If so set them.... + if (count($params) > 0) + { + foreach ($prefs as $key => $val) + { + if (isset($params[$key])) + { + $prefs[$key] = $params[$key]; + } + } + } + + // Are we backing up a complete database or individual tables? + // If no table names were submitted we'll fetch the entire table list + if (count($prefs['tables']) === 0) + { + $prefs['tables'] = $this->db->list_tables(); + } + + // Validate the format + if ( ! in_array($prefs['format'], array('gzip', 'zip', 'txt'), TRUE)) + { + $prefs['format'] = 'txt'; + } + + // Is the encoder supported? If not, we'll either issue an + // error or use plain text depending on the debug settings + if (($prefs['format'] === 'gzip' && ! function_exists('gzencode')) + OR ($prefs['format'] === 'zip' && ! function_exists('gzcompress'))) + { + if ($this->db->db_debug) + { + return $this->db->display_error('db_unsupported_compression'); + } + + $prefs['format'] = 'txt'; + } + + // Was a Zip file requested? + if ($prefs['format'] === 'zip') + { + // Set the filename if not provided (only needed with Zip files) + if ($prefs['filename'] === '') + { + $prefs['filename'] = (count($prefs['tables']) === 1 ? $prefs['tables'] : $this->db->database) + .date('Y-m-d_H-i', time()).'.sql'; + } + else + { + // If they included the .zip file extension we'll remove it + if (preg_match('|.+?\.zip$|', $prefs['filename'])) + { + $prefs['filename'] = str_replace('.zip', '', $prefs['filename']); + } + + // Tack on the ".sql" file extension if needed + if ( ! preg_match('|.+?\.sql$|', $prefs['filename'])) + { + $prefs['filename'] .= '.sql'; + } + } + + // Load the Zip class and output it + $CI =& get_instance(); + $CI->load->library('zip'); + $CI->zip->add_data($prefs['filename'], $this->_backup($prefs)); + return $CI->zip->get_zip(); + } + elseif ($prefs['format'] === 'txt') // Was a text file requested? + { + return $this->_backup($prefs); + } + elseif ($prefs['format'] === 'gzip') // Was a Gzip file requested? + { + return gzencode($this->_backup($prefs)); + } + + return; + } + +} diff --git a/system/database/drivers/cubrid/cubrid_driver.php b/system/database/drivers/cubrid/cubrid_driver.php new file mode 100644 index 0000000..77f591c --- /dev/null +++ b/system/database/drivers/cubrid/cubrid_driver.php @@ -0,0 +1,405 @@ +dsn, $matches)) + { + if (stripos($matches[2], 'autocommit=off') !== FALSE) + { + $this->auto_commit = FALSE; + } + } + else + { + // If no port is defined by the user, use the default value + empty($this->port) OR $this->port = 33000; + } + } + + // -------------------------------------------------------------------- + + /** + * Non-persistent database connection + * + * @param bool $persistent + * @return resource + */ + public function db_connect($persistent = FALSE) + { + if (preg_match('/^CUBRID:[^:]+(:[0-9][1-9]{0,4})?:[^:]+:([^:]*):([^:]*):(\?.+)?$/', $this->dsn, $matches)) + { + $func = ($persistent !== TRUE) ? 'cubrid_connect_with_url' : 'cubrid_pconnect_with_url'; + return ($matches[2] === '' && $matches[3] === '' && $this->username !== '' && $this->password !== '') + ? $func($this->dsn, $this->username, $this->password) + : $func($this->dsn); + } + + $func = ($persistent !== TRUE) ? 'cubrid_connect' : 'cubrid_pconnect'; + return ($this->username !== '') + ? $func($this->hostname, $this->port, $this->database, $this->username, $this->password) + : $func($this->hostname, $this->port, $this->database); + } + + // -------------------------------------------------------------------- + + /** + * Reconnect + * + * Keep / reestablish the db connection if no queries have been + * sent for a length of time exceeding the server's idle timeout + * + * @return void + */ + public function reconnect() + { + if (cubrid_ping($this->conn_id) === FALSE) + { + $this->conn_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + return ( ! $this->conn_id OR ($version = cubrid_get_server_info($this->conn_id)) === FALSE) + ? FALSE + : $this->data_cache['version'] = $version; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return resource + */ + protected function _execute($sql) + { + return cubrid_query($sql, $this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + if (($autocommit = cubrid_get_autocommit($this->conn_id)) === NULL) + { + return FALSE; + } + elseif ($autocommit === TRUE) + { + return cubrid_set_autocommit($this->conn_id, CUBRID_AUTOCOMMIT_FALSE); + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + if ( ! cubrid_commit($this->conn_id)) + { + return FALSE; + } + + if ($this->auto_commit && ! cubrid_get_autocommit($this->conn_id)) + { + return cubrid_set_autocommit($this->conn_id, CUBRID_AUTOCOMMIT_TRUE); + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + if ( ! cubrid_rollback($this->conn_id)) + { + return FALSE; + } + + if ($this->auto_commit && ! cubrid_get_autocommit($this->conn_id)) + { + cubrid_set_autocommit($this->conn_id, CUBRID_AUTOCOMMIT_TRUE); + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependant string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + return cubrid_real_escape_string($str, $this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return cubrid_affected_rows(); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return int + */ + public function insert_id() + { + return cubrid_insert_id($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * List table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SHOW TABLES'; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql." LIKE '".$this->escape_like_str($this->dbprefix)."%'"; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE))) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->Field; + + sscanf($query[$i]->Type, '%[a-z](%d)', + $retval[$i]->type, + $retval[$i]->max_length + ); + + $retval[$i]->default = $query[$i]->Default; + $retval[$i]->primary_key = (int) ($query[$i]->Key === 'PRI'); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occured. + * + * @return array + */ + public function error() + { + return array('code' => cubrid_errno($this->conn_id), 'message' => cubrid_error($this->conn_id)); + } + + // -------------------------------------------------------------------- + + /** + * FROM tables + * + * Groups tables in FROM clauses if needed, so there is no confusion + * about operator precedence. + * + * @return string + */ + protected function _from_tables() + { + if ( ! empty($this->qb_join) && count($this->qb_from) > 1) + { + return '('.implode(', ', $this->qb_from).')'; + } + + return implode(', ', $this->qb_from); + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + cubrid_close($this->conn_id); + } + +} diff --git a/system/database/drivers/cubrid/cubrid_forge.php b/system/database/drivers/cubrid/cubrid_forge.php new file mode 100644 index 0000000..46a3b21 --- /dev/null +++ b/system/database/drivers/cubrid/cubrid_forge.php @@ -0,0 +1,230 @@ + 'INTEGER', + 'SMALLINT' => 'INTEGER', + 'INT' => 'BIGINT', + 'INTEGER' => 'BIGINT', + 'BIGINT' => 'NUMERIC', + 'FLOAT' => 'DOUBLE', + 'REAL' => 'DOUBLE' + ); + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('DROP', 'ADD'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + $sqls[] = $sql.' CHANGE '.$field[$i]['_literal']; + } + else + { + $alter_type = empty($field[$i]['new_name']) ? ' MODIFY ' : ' CHANGE '; + $sqls[] = $sql.$alter_type.$this->_process_column($field[$i]); + } + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + $extra_clause = isset($field['after']) + ? ' AFTER '.$this->db->escape_identifiers($field['after']) : ''; + + if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE) + { + $extra_clause = ' FIRST'; + } + + return $this->db->escape_identifiers($field['name']) + .(empty($field['new_name']) ? '' : ' '.$this->db->escape_identifiers($field['new_name'])) + .' '.$field['type'].$field['length'] + .$field['unsigned'] + .$field['null'] + .$field['default'] + .$field['auto_increment'] + .$field['unique'] + .$extra_clause; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'LONGTEXT': + $attributes['TYPE'] = 'STRING'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Process indexes + * + * @param string $table (ignored) + * @return string + */ + protected function _process_indexes($table) + { + $sql = ''; + + for ($i = 0, $c = count($this->keys); $i < $c; $i++) + { + if (is_array($this->keys[$i])) + { + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) + { + if ( ! isset($this->fields[$this->keys[$i][$i2]])) + { + unset($this->keys[$i][$i2]); + continue; + } + } + } + elseif ( ! isset($this->fields[$this->keys[$i]])) + { + unset($this->keys[$i]); + continue; + } + + is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]); + + $sql .= ",\n\tKEY ".$this->db->escape_identifiers(implode('_', $this->keys[$i])) + .' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).')'; + } + + $this->keys = array(); + + return $sql; + } + +} diff --git a/system/database/drivers/cubrid/cubrid_result.php b/system/database/drivers/cubrid/cubrid_result.php new file mode 100644 index 0000000..9cccb25 --- /dev/null +++ b/system/database/drivers/cubrid/cubrid_result.php @@ -0,0 +1,177 @@ +num_rows) + ? $this->num_rows + : $this->num_rows = cubrid_num_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return cubrid_num_fields($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + return cubrid_column_names($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = cubrid_field_name($this->result_id, $i); + $retval[$i]->type = cubrid_field_type($this->result_id, $i); + $retval[$i]->max_length = cubrid_field_len($this->result_id, $i); + $retval[$i]->primary_key = (int) (strpos(cubrid_field_flags($this->result_id, $i), 'primary_key') !== FALSE); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_resource($this->result_id) OR + (get_resource_type($this->result_id) === 'Unknown' && preg_match('/Resource id #/', strval($this->result_id)))) + { + cubrid_close_request($this->result_id); + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * @param int $n + * @return bool + */ + public function data_seek($n = 0) + { + return cubrid_data_seek($this->result_id, $n); + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return cubrid_fetch_assoc($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return cubrid_fetch_object($this->result_id, $class_name); + } + +} diff --git a/system/database/drivers/cubrid/cubrid_utility.php b/system/database/drivers/cubrid/cubrid_utility.php new file mode 100644 index 0000000..942fa3b --- /dev/null +++ b/system/database/drivers/cubrid/cubrid_utility.php @@ -0,0 +1,79 @@ +db->data_cache['db_names'])) + { + return $this->db->data_cache['db_names']; + } + + return $this->db->data_cache['db_names'] = cubrid_list_dbs($this->db->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * CUBRID Export + * + * @param array Preferences + * @return mixed + */ + protected function _backup($params = array()) + { + // No SQL based support in CUBRID as of version 8.4.0. Database or + // table backup can be performed using CUBRID Manager + // database administration tool. + return $this->db->display_error('db_unsupported_feature'); + } +} diff --git a/system/database/drivers/cubrid/index.html b/system/database/drivers/cubrid/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/cubrid/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/ibase/ibase_driver.php b/system/database/drivers/ibase/ibase_driver.php new file mode 100644 index 0000000..671a353 --- /dev/null +++ b/system/database/drivers/ibase/ibase_driver.php @@ -0,0 +1,413 @@ +hostname.':'.$this->database, $this->username, $this->password, $this->char_set) + : ibase_connect($this->hostname.':'.$this->database, $this->username, $this->password, $this->char_set); + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + if (($service = ibase_service_attach($this->hostname, $this->username, $this->password))) + { + $this->data_cache['version'] = ibase_server_info($service, IBASE_SVC_SERVER_VERSION); + + // Don't keep the service open + ibase_service_detach($service); + return $this->data_cache['version']; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return resource + */ + protected function _execute($sql) + { + return ibase_query(isset($this->_ibase_trans) ? $this->_ibase_trans : $this->conn_id, $sql); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + if (($trans_handle = ibase_trans($this->conn_id)) === FALSE) + { + return FALSE; + } + + $this->_ibase_trans = $trans_handle; + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + if (ibase_commit($this->_ibase_trans)) + { + $this->_ibase_trans = NULL; + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + if (ibase_rollback($this->_ibase_trans)) + { + $this->_ibase_trans = NULL; + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return ibase_affected_rows($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @param string $generator_name + * @param int $inc_by + * @return int + */ + public function insert_id($generator_name, $inc_by = 0) + { + //If a generator hasn't been used before it will return 0 + return ibase_gen_id('"'.$generator_name.'"', $inc_by); + } + + // -------------------------------------------------------------------- + + /** + * List table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT TRIM("RDB$RELATION_NAME") AS TABLE_NAME FROM "RDB$RELATIONS" WHERE "RDB$RELATION_NAME" NOT LIKE \'RDB$%\' AND "RDB$RELATION_NAME" NOT LIKE \'MON$%\''; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql.' AND TRIM("RDB$RELATION_NAME") AS TABLE_NAME LIKE \''.$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT TRIM("RDB$FIELD_NAME") AS COLUMN_NAME FROM "RDB$RELATION_FIELDS" WHERE "RDB$RELATION_NAME" = '.$this->escape($table); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT "rfields"."RDB$FIELD_NAME" AS "name", + CASE "fields"."RDB$FIELD_TYPE" + WHEN 7 THEN \'SMALLINT\' + WHEN 8 THEN \'INTEGER\' + WHEN 9 THEN \'QUAD\' + WHEN 10 THEN \'FLOAT\' + WHEN 11 THEN \'DFLOAT\' + WHEN 12 THEN \'DATE\' + WHEN 13 THEN \'TIME\' + WHEN 14 THEN \'CHAR\' + WHEN 16 THEN \'INT64\' + WHEN 27 THEN \'DOUBLE\' + WHEN 35 THEN \'TIMESTAMP\' + WHEN 37 THEN \'VARCHAR\' + WHEN 40 THEN \'CSTRING\' + WHEN 261 THEN \'BLOB\' + ELSE NULL + END AS "type", + "fields"."RDB$FIELD_LENGTH" AS "max_length", + "rfields"."RDB$DEFAULT_VALUE" AS "default" + FROM "RDB$RELATION_FIELDS" "rfields" + JOIN "RDB$FIELDS" "fields" ON "rfields"."RDB$FIELD_SOURCE" = "fields"."RDB$FIELD_NAME" + WHERE "rfields"."RDB$RELATION_NAME" = '.$this->escape($table).' + ORDER BY "rfields"."RDB$FIELD_POSITION"'; + + return (($query = $this->query($sql)) !== FALSE) + ? $query->result_object() + : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occured. + * + * @return array + */ + public function error() + { + return array('code' => ibase_errcode(), 'message' => ibase_errmsg()); + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'DELETE FROM '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + $this->qb_limit = FALSE; + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + // Limit clause depends on if Interbase or Firebird + if (stripos($this->version(), 'firebird') !== FALSE) + { + $select = 'FIRST '.$this->qb_limit + .($this->qb_offset ? ' SKIP '.$this->qb_offset : ''); + } + else + { + $select = 'ROWS ' + .($this->qb_offset ? $this->qb_offset.' TO '.($this->qb_limit + $this->qb_offset) : $this->qb_limit); + } + + return preg_replace('`SELECT`i', 'SELECT '.$select, $sql, 1); + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string|bool + */ + protected function _insert_batch($table, $keys, $values) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + ibase_close($this->conn_id); + } + +} diff --git a/system/database/drivers/ibase/ibase_forge.php b/system/database/drivers/ibase/ibase_forge.php new file mode 100644 index 0000000..b35cc37 --- /dev/null +++ b/system/database/drivers/ibase/ibase_forge.php @@ -0,0 +1,251 @@ + 'INTEGER', + 'INTEGER' => 'INT64', + 'FLOAT' => 'DOUBLE PRECISION' + ); + + /** + * NULL value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $_null = 'NULL'; + + // -------------------------------------------------------------------- + + /** + * Create database + * + * @param string $db_name + * @return string + */ + public function create_database($db_name) + { + // Firebird databases are flat files, so a path is required + + // Hostname is needed for remote access + empty($this->db->hostname) OR $db_name = $this->hostname.':'.$db_name; + + return parent::create_database('"'.$db_name.'"'); + } + + // -------------------------------------------------------------------- + + /** + * Drop database + * + * @param string $db_name (ignored) + * @return bool + */ + public function drop_database($db_name) + { + if ( ! ibase_drop_db($this->conn_id)) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + elseif ( ! empty($this->db->data_cache['db_names'])) + { + $key = array_search(strtolower($this->db->database), array_map('strtolower', $this->db->data_cache['db_names']), TRUE); + if ($key !== FALSE) + { + unset($this->db->data_cache['db_names'][$key]); + } + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('DROP', 'ADD'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + return FALSE; + } + + if (isset($field[$i]['type'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identififers($field[$i]['name']) + .' TYPE '.$field[$i]['type'].$field[$i]['length']; + } + + if ( ! empty($field[$i]['default'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' SET DEFAULT '.$field[$i]['default']; + } + + if (isset($field[$i]['null'])) + { + $sqls[] = 'UPDATE "RDB$RELATION_FIELDS" SET "RDB$NULL_FLAG" = ' + .($field[$i]['null'] === TRUE ? 'NULL' : '1') + .' WHERE "RDB$FIELD_NAME" = '.$this->db->escape($field[$i]['name']) + .' AND "RDB$RELATION_NAME" = '.$this->db->escape($table); + } + + if ( ! empty($field[$i]['new_name'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TO '.$this->db->escape_identifiers($field[$i]['new_name']); + } + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'].$field['length'] + .$field['null'] + .$field['unique'] + .$field['default']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'INT': + $attributes['TYPE'] = 'INTEGER'; + return; + case 'BIGINT': + $attributes['TYPE'] = 'INT64'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + // Not supported + } + +} diff --git a/system/database/drivers/ibase/ibase_result.php b/system/database/drivers/ibase/ibase_result.php new file mode 100644 index 0000000..f3c21fc --- /dev/null +++ b/system/database/drivers/ibase/ibase_result.php @@ -0,0 +1,161 @@ +result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + for ($i = 0, $num_fields = $this->num_fields(); $i < $num_fields; $i++) + { + $info = ibase_field_info($this->result_id, $i); + $field_names[] = $info['name']; + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $info = ibase_field_info($this->result_id, $i); + + $retval[$i] = new stdClass(); + $retval[$i]->name = $info['name']; + $retval[$i]->type = $info['type']; + $retval[$i]->max_length = $info['length']; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + ibase_free_result($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return ibase_fetch_assoc($this->result_id, IBASE_FETCH_BLOBS); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + $row = ibase_fetch_object($this->result_id, IBASE_FETCH_BLOBS); + + if ($class_name === 'stdClass' OR ! $row) + { + return $row; + } + + $class_name = new $class_name(); + foreach ($row as $key => $value) + { + $class_name->$key = $value; + } + + return $class_name; + } + +} diff --git a/system/database/drivers/ibase/ibase_utility.php b/system/database/drivers/ibase/ibase_utility.php new file mode 100644 index 0000000..619ebad --- /dev/null +++ b/system/database/drivers/ibase/ibase_utility.php @@ -0,0 +1,69 @@ +db->hostname, $this->db->username, $this->db->password)) + { + $res = ibase_backup($service, $this->db->database, $filename.'.fbk'); + + // Close the service connection + ibase_service_detach($service); + return $res; + } + + return FALSE; + } + +} diff --git a/system/database/drivers/ibase/index.html b/system/database/drivers/ibase/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/ibase/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/index.html b/system/database/drivers/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/mssql/index.html b/system/database/drivers/mssql/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/mssql/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/mssql/mssql_driver.php b/system/database/drivers/mssql/mssql_driver.php new file mode 100644 index 0000000..66d7572 --- /dev/null +++ b/system/database/drivers/mssql/mssql_driver.php @@ -0,0 +1,518 @@ +port)) + { + $this->hostname .= (DIRECTORY_SEPARATOR === '\\' ? ',' : ':').$this->port; + } + } + + // -------------------------------------------------------------------- + + /** + * Non-persistent database connection + * + * @param bool $persistent + * @return resource + */ + public function db_connect($persistent = FALSE) + { + $this->conn_id = ($persistent) + ? mssql_pconnect($this->hostname, $this->username, $this->password) + : mssql_connect($this->hostname, $this->username, $this->password); + + if ( ! $this->conn_id) + { + return FALSE; + } + + // ---------------------------------------------------------------- + + // Select the DB... assuming a database name is specified in the config file + if ($this->database !== '' && ! $this->db_select()) + { + log_message('error', 'Unable to select database: '.$this->database); + + return ($this->db_debug === TRUE) + ? $this->display_error('db_unable_to_select', $this->database) + : FALSE; + } + + // Determine how identifiers are escaped + $query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi'); + $query = $query->row_array(); + $this->_quoted_identifier = empty($query) ? FALSE : (bool) $query['qi']; + $this->_escape_char = ($this->_quoted_identifier) ? '"' : array('[', ']'); + + return $this->conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Select the database + * + * @param string $database + * @return bool + */ + public function db_select($database = '') + { + if ($database === '') + { + $database = $this->database; + } + + // Note: Escaping is required in the event that the DB name + // contains reserved characters. + if (mssql_select_db('['.$database.']', $this->conn_id)) + { + $this->database = $database; + $this->data_cache = array(); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return mixed resource if rows are returned, bool otherwise + */ + protected function _execute($sql) + { + return mssql_query($sql, $this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + return $this->simple_query('BEGIN TRAN'); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + return $this->simple_query('COMMIT TRAN'); + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + return $this->simple_query('ROLLBACK TRAN'); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return mssql_rows_affected($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * Returns the last id created in the Identity column. + * + * @return string + */ + public function insert_id() + { + $query = version_compare($this->version(), '8', '>=') + ? 'SELECT SCOPE_IDENTITY() AS last_id' + : 'SELECT @@IDENTITY AS last_id'; + + $query = $this->query($query); + $query = $query->row(); + return $query->last_id; + } + + // -------------------------------------------------------------------- + + /** + * Set client character set + * + * @param string $charset + * @return bool + */ + protected function _db_set_charset($charset) + { + return (ini_set('mssql.charset', $charset) !== FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Version number query string + * + * @return string + */ + protected function _version() + { + return "SELECT SERVERPROPERTY('ProductVersion') AS ver"; + } + + // -------------------------------------------------------------------- + + /** + * List table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT '.$this->escape_identifiers('name') + .' FROM '.$this->escape_identifiers('sysobjects') + .' WHERE '.$this->escape_identifiers('type')." = 'U'"; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + $sql .= ' AND '.$this->escape_identifiers('name')." LIKE '".$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql.' ORDER BY '.$this->escape_identifiers('name'); + } + + // -------------------------------------------------------------------- + + /** + * List column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->COLUMN_NAME; + $retval[$i]->type = $query[$i]->DATA_TYPE; + $retval[$i]->max_length = ($query[$i]->CHARACTER_MAXIMUM_LENGTH > 0) ? $query[$i]->CHARACTER_MAXIMUM_LENGTH : $query[$i]->NUMERIC_PRECISION; + $retval[$i]->default = $query[$i]->COLUMN_DEFAULT; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occured. + * + * @return array + */ + public function error() + { + // We need this because the error info is discarded by the + // server the first time you request it, and query() already + // calls error() once for logging purposes when a query fails. + static $error = array('code' => 0, 'message' => NULL); + + $message = mssql_get_last_message(); + if ( ! empty($message)) + { + $error['code'] = $this->query('SELECT @@ERROR AS code')->row()->code; + $error['message'] = $message; + } + + return $error; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE TABLE '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + if ($this->qb_limit) + { + return 'WITH ci_delete AS (SELECT TOP '.$this->qb_limit.' * FROM '.$table.$this->_compile_wh('qb_where').') DELETE FROM ci_delete'; + } + + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + $limit = $this->qb_offset + $this->qb_limit; + + // As of SQL Server 2005 (9.0.*) ROW_NUMBER() is supported, + // however an ORDER BY clause is required for it to work + if (version_compare($this->version(), '9', '>=') && $this->qb_offset && ! empty($this->qb_orderby)) + { + $orderby = $this->_compile_order_by(); + + // We have to strip the ORDER BY clause + $sql = trim(substr($sql, 0, strrpos($sql, $orderby))); + + // Get the fields to select from our subquery, so that we can avoid CI_rownum appearing in the actual results + if (count($this->qb_select) === 0) + { + $select = '*'; // Inevitable + } + else + { + // Use only field names and their aliases, everything else is out of our scope. + $select = array(); + $field_regexp = ($this->_quoted_identifier) + ? '("[^\"]+")' : '(\[[^\]]+\])'; + for ($i = 0, $c = count($this->qb_select); $i < $c; $i++) + { + $select[] = preg_match('/(?:\s|\.)'.$field_regexp.'$/i', $this->qb_select[$i], $m) + ? $m[1] : $this->qb_select[$i]; + } + $select = implode(', ', $select); + } + + return 'SELECT '.$select." FROM (\n\n" + .preg_replace('/^(SELECT( DISTINCT)?)/i', '\\1 ROW_NUMBER() OVER('.trim($orderby).') AS '.$this->escape_identifiers('CI_rownum').', ', $sql) + ."\n\n) ".$this->escape_identifiers('CI_subquery') + ."\nWHERE ".$this->escape_identifiers('CI_rownum').' BETWEEN '.($this->qb_offset + 1).' AND '.$limit; + } + + return preg_replace('/(^\SELECT (DISTINCT)?)/i','\\1 TOP '.$limit.' ', $sql); + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string|bool + */ + protected function _insert_batch($table, $keys, $values) + { + // Multiple-value inserts are only supported as of SQL Server 2008 + if (version_compare($this->version(), '10', '>=')) + { + return parent::_insert_batch($table, $keys, $values); + } + + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + mssql_close($this->conn_id); + } + +} diff --git a/system/database/drivers/mssql/mssql_forge.php b/system/database/drivers/mssql/mssql_forge.php new file mode 100644 index 0000000..91b5794 --- /dev/null +++ b/system/database/drivers/mssql/mssql_forge.php @@ -0,0 +1,151 @@ + 'SMALLINT', + 'SMALLINT' => 'INT', + 'INT' => 'BIGINT', + 'REAL' => 'FLOAT' + ); + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('ADD', 'DROP'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' ALTER COLUMN '; + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + $sqls[] = $sql.$this->_process_column($field[$i]); + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + if (isset($attributes['CONSTRAINT']) && strpos($attributes['TYPE'], 'INT') !== FALSE) + { + unset($attributes['CONSTRAINT']); + } + + switch (strtoupper($attributes['TYPE'])) + { + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'INTEGER': + $attributes['TYPE'] = 'INT'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['auto_increment'] = ' IDENTITY(1,1)'; + } + } + +} diff --git a/system/database/drivers/mssql/mssql_result.php b/system/database/drivers/mssql/mssql_result.php new file mode 100644 index 0000000..b62bf75 --- /dev/null +++ b/system/database/drivers/mssql/mssql_result.php @@ -0,0 +1,198 @@ +num_rows) + ? $this->num_rows + : $this->num_rows = mssql_num_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return mssql_num_fields($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + mssql_field_seek($this->result_id, 0); + while ($field = mssql_fetch_field($this->result_id)) + { + $field_names[] = $field->name; + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $field = mssql_fetch_field($this->result_id, $i); + + $retval[$i] = new stdClass(); + $retval[$i]->name = $field->name; + $retval[$i]->type = $field->type; + $retval[$i]->max_length = $field->max_length; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_resource($this->result_id)) + { + mssql_free_result($this->result_id); + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * @param int $n + * @return bool + */ + public function data_seek($n = 0) + { + return mssql_data_seek($this->result_id, $n); + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return mssql_fetch_assoc($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + $row = mssql_fetch_object($this->result_id); + + if ($class_name === 'stdClass' OR ! $row) + { + return $row; + } + + $class_name = new $class_name(); + foreach ($row as $key => $value) + { + $class_name->$key = $value; + } + + return $class_name; + } + +} diff --git a/system/database/drivers/mssql/mssql_utility.php b/system/database/drivers/mssql/mssql_utility.php new file mode 100644 index 0000000..cd23be8 --- /dev/null +++ b/system/database/drivers/mssql/mssql_utility.php @@ -0,0 +1,77 @@ +db->display_error('db_unsupported_feature'); + } + +} diff --git a/system/database/drivers/mysql/index.html b/system/database/drivers/mysql/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/mysql/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/mysql/mysql_driver.php b/system/database/drivers/mysql/mysql_driver.php new file mode 100644 index 0000000..7804dda --- /dev/null +++ b/system/database/drivers/mysql/mysql_driver.php @@ -0,0 +1,494 @@ +port)) + { + $this->hostname .= ':'.$this->port; + } + } + + // -------------------------------------------------------------------- + + /** + * Non-persistent database connection + * + * @param bool $persistent + * @return resource + */ + public function db_connect($persistent = FALSE) + { + $client_flags = ($this->compress === FALSE) ? 0 : MYSQL_CLIENT_COMPRESS; + + if ($this->encrypt === TRUE) + { + $client_flags = $client_flags | MYSQL_CLIENT_SSL; + } + + // Error suppression is necessary mostly due to PHP 5.5+ issuing E_DEPRECATED messages + $this->conn_id = ($persistent === TRUE) + ? mysql_pconnect($this->hostname, $this->username, $this->password, $client_flags) + : mysql_connect($this->hostname, $this->username, $this->password, TRUE, $client_flags); + + // ---------------------------------------------------------------- + + // Select the DB... assuming a database name is specified in the config file + if ($this->database !== '' && ! $this->db_select()) + { + log_message('error', 'Unable to select database: '.$this->database); + + return ($this->db_debug === TRUE) + ? $this->display_error('db_unable_to_select', $this->database) + : FALSE; + } + + if (isset($this->stricton) && is_resource($this->conn_id)) + { + if ($this->stricton) + { + $this->simple_query('SET SESSION sql_mode = CONCAT(@@sql_mode, ",", "STRICT_ALL_TABLES")'); + } + else + { + $this->simple_query( + 'SET SESSION sql_mode = + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + @@sql_mode, + "STRICT_ALL_TABLES,", ""), + ",STRICT_ALL_TABLES", ""), + "STRICT_ALL_TABLES", ""), + "STRICT_TRANS_TABLES,", ""), + ",STRICT_TRANS_TABLES", ""), + "STRICT_TRANS_TABLES", "")' + ); + } + } + + return $this->conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Reconnect + * + * Keep / reestablish the db connection if no queries have been + * sent for a length of time exceeding the server's idle timeout + * + * @return void + */ + public function reconnect() + { + if (mysql_ping($this->conn_id) === FALSE) + { + $this->conn_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Select the database + * + * @param string $database + * @return bool + */ + public function db_select($database = '') + { + if ($database === '') + { + $database = $this->database; + } + + if (mysql_select_db($database, $this->conn_id)) + { + $this->database = $database; + $this->data_cache = array(); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Set client character set + * + * @param string $charset + * @return bool + */ + protected function _db_set_charset($charset) + { + return mysql_set_charset($charset, $this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + if ( ! $this->conn_id OR ($version = mysql_get_server_info($this->conn_id)) === FALSE) + { + return FALSE; + } + + return $this->data_cache['version'] = $version; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return mixed + */ + protected function _execute($sql) + { + return mysql_query($this->_prep_query($sql), $this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Prep the query + * + * If needed, each database adapter can prep the query string + * + * @param string $sql an SQL query + * @return string + */ + protected function _prep_query($sql) + { + // mysql_affected_rows() returns 0 for "DELETE FROM TABLE" queries. This hack + // modifies the query so that it a proper number of affected rows is returned. + if ($this->delete_hack === TRUE && preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql)) + { + return trim($sql).' WHERE 1=1'; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + $this->simple_query('SET AUTOCOMMIT=0'); + return $this->simple_query('START TRANSACTION'); // can also be BEGIN or BEGIN WORK + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + if ($this->simple_query('COMMIT')) + { + $this->simple_query('SET AUTOCOMMIT=1'); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + if ($this->simple_query('ROLLBACK')) + { + $this->simple_query('SET AUTOCOMMIT=1'); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependant string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + return mysql_real_escape_string($str, $this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return mysql_affected_rows($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return int + */ + public function insert_id() + { + return mysql_insert_id($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * List table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SHOW TABLES FROM '.$this->escape_identifiers($this->database); + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql." LIKE '".$this->escape_like_str($this->dbprefix)."%'"; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE))) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->Field; + + sscanf($query[$i]->Type, '%[a-z](%d)', + $retval[$i]->type, + $retval[$i]->max_length + ); + + $retval[$i]->default = $query[$i]->Default; + $retval[$i]->primary_key = (int) ($query[$i]->Key === 'PRI'); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occured. + * + * @return array + */ + public function error() + { + return array('code' => mysql_errno($this->conn_id), 'message' => mysql_error($this->conn_id)); + } + + // -------------------------------------------------------------------- + + /** + * FROM tables + * + * Groups tables in FROM clauses if needed, so there is no confusion + * about operator precedence. + * + * @return string + */ + protected function _from_tables() + { + if ( ! empty($this->qb_join) && count($this->qb_from) > 1) + { + return '('.implode(', ', $this->qb_from).')'; + } + + return implode(', ', $this->qb_from); + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + // Error suppression to avoid annoying E_WARNINGs in cases + // where the connection has already been closed for some reason. + @mysql_close($this->conn_id); + } + +} diff --git a/system/database/drivers/mysql/mysql_forge.php b/system/database/drivers/mysql/mysql_forge.php new file mode 100644 index 0000000..fa84be3 --- /dev/null +++ b/system/database/drivers/mysql/mysql_forge.php @@ -0,0 +1,243 @@ +db->char_set) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET')) + { + $sql .= ' DEFAULT CHARACTER SET = '.$this->db->char_set; + } + + if ( ! empty($this->db->dbcollat) && ! strpos($sql, 'COLLATE')) + { + $sql .= ' COLLATE = '.$this->db->dbcollat; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'DROP') + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + $field[$i] = ($alter_type === 'ADD') + ? "\n\tADD ".$field[$i]['_literal'] + : "\n\tMODIFY ".$field[$i]['_literal']; + } + else + { + if ($alter_type === 'ADD') + { + $field[$i]['_literal'] = "\n\tADD "; + } + else + { + $field[$i]['_literal'] = empty($field[$i]['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE "; + } + + $field[$i] = $field[$i]['_literal'].$this->_process_column($field[$i]); + } + } + + return array($sql.implode(',', $field)); + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + $extra_clause = isset($field['after']) + ? ' AFTER '.$this->db->escape_identifiers($field['after']) : ''; + + if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE) + { + $extra_clause = ' FIRST'; + } + + + return $this->db->escape_identifiers($field['name']) + .(empty($field['new_name']) ? '' : ' '.$this->db->escape_identifiers($field['new_name'])) + .' '.$field['type'].$field['length'] + .$field['unsigned'] + .$field['null'] + .$field['default'] + .$field['auto_increment'] + .$field['unique'] + .(empty($field['comment']) ? '' : ' COMMENT '.$field['comment']) + .$extra_clause; + } + + // -------------------------------------------------------------------- + + /** + * Process indexes + * + * @param string $table (ignored) + * @return string + */ + protected function _process_indexes($table) + { + $sql = ''; + + for ($i = 0, $c = count($this->keys); $i < $c; $i++) + { + if (is_array($this->keys[$i])) + { + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) + { + if ( ! isset($this->fields[$this->keys[$i][$i2]])) + { + unset($this->keys[$i][$i2]); + continue; + } + } + } + elseif ( ! isset($this->fields[$this->keys[$i]])) + { + unset($this->keys[$i]); + continue; + } + + is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]); + + $sql .= ",\n\tKEY ".$this->db->escape_identifiers(implode('_', $this->keys[$i])) + .' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).')'; + } + + $this->keys = array(); + + return $sql; + } + +} diff --git a/system/database/drivers/mysql/mysql_result.php b/system/database/drivers/mysql/mysql_result.php new file mode 100644 index 0000000..20cade2 --- /dev/null +++ b/system/database/drivers/mysql/mysql_result.php @@ -0,0 +1,199 @@ +num_rows = mysql_num_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Number of rows in the result set + * + * @return int + */ + public function num_rows() + { + return $this->num_rows; + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return mysql_num_fields($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + mysql_field_seek($this->result_id, 0); + while ($field = mysql_fetch_field($this->result_id)) + { + $field_names[] = $field->name; + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = mysql_field_name($this->result_id, $i); + $retval[$i]->type = mysql_field_type($this->result_id, $i); + $retval[$i]->max_length = mysql_field_len($this->result_id, $i); + $retval[$i]->primary_key = (int) (strpos(mysql_field_flags($this->result_id, $i), 'primary_key') !== FALSE); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_resource($this->result_id)) + { + mysql_free_result($this->result_id); + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * @param int $n + * @return bool + */ + public function data_seek($n = 0) + { + return $this->num_rows + ? mysql_data_seek($this->result_id, $n) + : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return mysql_fetch_assoc($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return mysql_fetch_object($this->result_id, $class_name); + } + +} diff --git a/system/database/drivers/mysql/mysql_utility.php b/system/database/drivers/mysql/mysql_utility.php new file mode 100644 index 0000000..4c1f239 --- /dev/null +++ b/system/database/drivers/mysql/mysql_utility.php @@ -0,0 +1,211 @@ +db->query('SHOW CREATE TABLE '.$this->db->escape_identifiers($this->db->database.'.'.$table)); + + // No result means the table name was invalid + if ($query === FALSE) + { + continue; + } + + // Write out the table schema + $output .= '#'.$newline.'# TABLE STRUCTURE FOR: '.$table.$newline.'#'.$newline.$newline; + + if ($add_drop === TRUE) + { + $output .= 'DROP TABLE IF EXISTS '.$this->db->protect_identifiers($table).';'.$newline.$newline; + } + + $i = 0; + $result = $query->result_array(); + foreach ($result[0] as $val) + { + if ($i++ % 2) + { + $output .= $val.';'.$newline.$newline; + } + } + + // If inserts are not needed we're done... + if ($add_insert === FALSE) + { + continue; + } + + // Grab all the data from the current table + $query = $this->db->query('SELECT * FROM '.$this->db->protect_identifiers($table)); + + if ($query->num_rows() === 0) + { + continue; + } + + // Fetch the field names and determine if the field is an + // integer type. We use this info to decide whether to + // surround the data with quotes or not + + $i = 0; + $field_str = ''; + $is_int = array(); + while ($field = mysql_fetch_field($query->result_id)) + { + // Most versions of MySQL store timestamp as a string + $is_int[$i] = in_array(strtolower(mysql_field_type($query->result_id, $i)), + array('tinyint', 'smallint', 'mediumint', 'int', 'bigint'), //, 'timestamp'), + TRUE); + + // Create a string of field names + $field_str .= $this->db->escape_identifiers($field->name).', '; + $i++; + } + + // Trim off the end comma + $field_str = preg_replace('/, $/' , '', $field_str); + + // Build the insert string + foreach ($query->result_array() as $row) + { + $val_str = ''; + + $i = 0; + foreach ($row as $v) + { + // Is the value NULL? + if ($v === NULL) + { + $val_str .= 'NULL'; + } + else + { + // Escape the data if it's not an integer + $val_str .= ($is_int[$i] === FALSE) ? $this->db->escape($v) : $v; + } + + // Append a comma + $val_str .= ', '; + $i++; + } + + // Remove the comma at the end of the string + $val_str = preg_replace('/, $/' , '', $val_str); + + // Build the INSERT string + $output .= 'INSERT INTO '.$this->db->protect_identifiers($table).' ('.$field_str.') VALUES ('.$val_str.');'.$newline; + } + + $output .= $newline.$newline; + } + + // Do we need to include a statement to re-enable foreign key checks? + if ($foreign_key_checks === FALSE) + { + $output .= 'SET foreign_key_checks = 1;'.$newline; + } + + return $output; + } + +} diff --git a/system/database/drivers/mysqli/index.html b/system/database/drivers/mysqli/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/mysqli/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/mysqli/mysqli_driver.php b/system/database/drivers/mysqli/mysqli_driver.php new file mode 100644 index 0000000..4a14eea --- /dev/null +++ b/system/database/drivers/mysqli/mysqli_driver.php @@ -0,0 +1,544 @@ +hostname[0] === '/') + { + $hostname = NULL; + $port = NULL; + $socket = $this->hostname; + } + else + { + $hostname = ($persistent === TRUE) + ? 'p:'.$this->hostname : $this->hostname; + $port = empty($this->port) ? NULL : $this->port; + $socket = NULL; + } + + $client_flags = ($this->compress === TRUE) ? MYSQLI_CLIENT_COMPRESS : 0; + $this->_mysqli = mysqli_init(); + + $this->_mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10); + + if (isset($this->stricton)) + { + if ($this->stricton) + { + $this->_mysqli->options(MYSQLI_INIT_COMMAND, 'SET SESSION sql_mode = CONCAT(@@sql_mode, ",", "STRICT_ALL_TABLES")'); + } + else + { + $this->_mysqli->options(MYSQLI_INIT_COMMAND, + 'SET SESSION sql_mode = + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + @@sql_mode, + "STRICT_ALL_TABLES,", ""), + ",STRICT_ALL_TABLES", ""), + "STRICT_ALL_TABLES", ""), + "STRICT_TRANS_TABLES,", ""), + ",STRICT_TRANS_TABLES", ""), + "STRICT_TRANS_TABLES", "")' + ); + } + } + + if (is_array($this->encrypt)) + { + $ssl = array(); + empty($this->encrypt['ssl_key']) OR $ssl['key'] = $this->encrypt['ssl_key']; + empty($this->encrypt['ssl_cert']) OR $ssl['cert'] = $this->encrypt['ssl_cert']; + empty($this->encrypt['ssl_ca']) OR $ssl['ca'] = $this->encrypt['ssl_ca']; + empty($this->encrypt['ssl_capath']) OR $ssl['capath'] = $this->encrypt['ssl_capath']; + empty($this->encrypt['ssl_cipher']) OR $ssl['cipher'] = $this->encrypt['ssl_cipher']; + + if ( ! empty($ssl)) + { + if (isset($this->encrypt['ssl_verify'])) + { + if ($this->encrypt['ssl_verify']) + { + defined('MYSQLI_OPT_SSL_VERIFY_SERVER_CERT') && $this->_mysqli->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, TRUE); + } + // Apparently (when it exists), setting MYSQLI_OPT_SSL_VERIFY_SERVER_CERT + // to FALSE didn't do anything, so PHP 5.6.16 introduced yet another + // constant ... + // + // https://secure.php.net/ChangeLog-5.php#5.6.16 + // https://bugs.php.net/bug.php?id=68344 + elseif (defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT')) + { + $client_flags |= MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT; + } + } + + $client_flags |= MYSQLI_CLIENT_SSL; + $this->_mysqli->ssl_set( + isset($ssl['key']) ? $ssl['key'] : NULL, + isset($ssl['cert']) ? $ssl['cert'] : NULL, + isset($ssl['ca']) ? $ssl['ca'] : NULL, + isset($ssl['capath']) ? $ssl['capath'] : NULL, + isset($ssl['cipher']) ? $ssl['cipher'] : NULL + ); + } + } + + if ($this->_mysqli->real_connect($hostname, $this->username, $this->password, $this->database, $port, $socket, $client_flags)) + { + // Prior to version 5.7.3, MySQL silently downgrades to an unencrypted connection if SSL setup fails + if ( + ($client_flags & MYSQLI_CLIENT_SSL) + && version_compare($this->_mysqli->client_info, '5.7.3', '<=') + && empty($this->_mysqli->query("SHOW STATUS LIKE 'ssl_cipher'")->fetch_object()->Value) + ) + { + $this->_mysqli->close(); + $message = 'MySQLi was configured for an SSL connection, but got an unencrypted connection instead!'; + log_message('error', $message); + return ($this->db->db_debug) ? $this->db->display_error($message, '', TRUE) : FALSE; + } + + return $this->_mysqli; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Reconnect + * + * Keep / reestablish the db connection if no queries have been + * sent for a length of time exceeding the server's idle timeout + * + * @return void + */ + public function reconnect() + { + if ($this->conn_id !== FALSE && $this->conn_id->ping() === FALSE) + { + $this->conn_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Select the database + * + * @param string $database + * @return bool + */ + public function db_select($database = '') + { + if ($database === '') + { + $database = $this->database; + } + + if ($this->conn_id->select_db($database)) + { + $this->database = $database; + $this->data_cache = array(); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Set client character set + * + * @param string $charset + * @return bool + */ + protected function _db_set_charset($charset) + { + return $this->conn_id->set_charset($charset); + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + return $this->data_cache['version'] = $this->conn_id->server_info; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return mixed + */ + protected function _execute($sql) + { + return $this->conn_id->query($this->_prep_query($sql)); + } + + // -------------------------------------------------------------------- + + /** + * Prep the query + * + * If needed, each database adapter can prep the query string + * + * @param string $sql an SQL query + * @return string + */ + protected function _prep_query($sql) + { + // mysqli_affected_rows() returns 0 for "DELETE FROM TABLE" queries. This hack + // modifies the query so that it a proper number of affected rows is returned. + if ($this->delete_hack === TRUE && preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql)) + { + return trim($sql).' WHERE 1=1'; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + $this->conn_id->autocommit(FALSE); + return is_php('5.5') + ? $this->conn_id->begin_transaction() + : $this->simple_query('START TRANSACTION'); // can also be BEGIN or BEGIN WORK + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + if ($this->conn_id->commit()) + { + $this->conn_id->autocommit(TRUE); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + if ($this->conn_id->rollback()) + { + $this->conn_id->autocommit(TRUE); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependant string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + return $this->conn_id->real_escape_string($str); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return $this->conn_id->affected_rows; + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return int + */ + public function insert_id() + { + return $this->conn_id->insert_id; + } + + // -------------------------------------------------------------------- + + /** + * List table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SHOW TABLES FROM '.$this->escape_identifiers($this->database); + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql." LIKE '".$this->escape_like_str($this->dbprefix)."%'"; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE))) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->Field; + + sscanf($query[$i]->Type, '%[a-z](%d)', + $retval[$i]->type, + $retval[$i]->max_length + ); + + $retval[$i]->default = $query[$i]->Default; + $retval[$i]->primary_key = (int) ($query[$i]->Key === 'PRI'); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occurred. + * + * @return array + */ + public function error() + { + if ( ! empty($this->_mysqli->connect_errno)) + { + return array( + 'code' => $this->_mysqli->connect_errno, + 'message' => $this->_mysqli->connect_error + ); + } + + return array('code' => $this->conn_id->errno, 'message' => $this->conn_id->error); + } + + // -------------------------------------------------------------------- + + /** + * FROM tables + * + * Groups tables in FROM clauses if needed, so there is no confusion + * about operator precedence. + * + * @return string + */ + protected function _from_tables() + { + if ( ! empty($this->qb_join) && count($this->qb_from) > 1) + { + return '('.implode(', ', $this->qb_from).')'; + } + + return implode(', ', $this->qb_from); + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + $this->conn_id->close(); + } + +} diff --git a/system/database/drivers/mysqli/mysqli_forge.php b/system/database/drivers/mysqli/mysqli_forge.php new file mode 100644 index 0000000..c17f729 --- /dev/null +++ b/system/database/drivers/mysqli/mysqli_forge.php @@ -0,0 +1,244 @@ +db->char_set) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET')) + { + $sql .= ' DEFAULT CHARACTER SET = '.$this->db->char_set; + } + + if ( ! empty($this->db->dbcollat) && ! strpos($sql, 'COLLATE')) + { + $sql .= ' COLLATE = '.$this->db->dbcollat; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'DROP') + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + $field[$i] = ($alter_type === 'ADD') + ? "\n\tADD ".$field[$i]['_literal'] + : "\n\tMODIFY ".$field[$i]['_literal']; + } + else + { + if ($alter_type === 'ADD') + { + $field[$i]['_literal'] = "\n\tADD "; + } + else + { + $field[$i]['_literal'] = empty($field[$i]['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE "; + } + + $field[$i] = $field[$i]['_literal'].$this->_process_column($field[$i]); + } + } + + return array($sql.implode(',', $field)); + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + $extra_clause = isset($field['after']) + ? ' AFTER '.$this->db->escape_identifiers($field['after']) : ''; + + if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE) + { + $extra_clause = ' FIRST'; + } + + return $this->db->escape_identifiers($field['name']) + .(empty($field['new_name']) ? '' : ' '.$this->db->escape_identifiers($field['new_name'])) + .' '.$field['type'].$field['length'] + .$field['unsigned'] + .$field['null'] + .$field['default'] + .$field['auto_increment'] + .$field['unique'] + .(empty($field['comment']) ? '' : ' COMMENT '.$field['comment']) + .$extra_clause; + } + + // -------------------------------------------------------------------- + + /** + * Process indexes + * + * @param string $table (ignored) + * @return string + */ + protected function _process_indexes($table) + { + $sql = ''; + + for ($i = 0, $c = count($this->keys); $i < $c; $i++) + { + if (is_array($this->keys[$i])) + { + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) + { + if ( ! isset($this->fields[$this->keys[$i][$i2]])) + { + unset($this->keys[$i][$i2]); + continue; + } + } + } + elseif ( ! isset($this->fields[$this->keys[$i]])) + { + unset($this->keys[$i]); + continue; + } + + is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]); + + $sql .= ",\n\tKEY ".$this->db->escape_identifiers(implode('_', $this->keys[$i])) + .' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).')'; + } + + $this->keys = array(); + + return $sql; + } + +} diff --git a/system/database/drivers/mysqli/mysqli_result.php b/system/database/drivers/mysqli/mysqli_result.php new file mode 100644 index 0000000..0ce0741 --- /dev/null +++ b/system/database/drivers/mysqli/mysqli_result.php @@ -0,0 +1,186 @@ +num_rows) + ? $this->num_rows + : $this->num_rows = $this->result_id->num_rows; + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return $this->result_id->field_count; + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + $this->result_id->field_seek(0); + while ($field = $this->result_id->fetch_field()) + { + $field_names[] = $field->name; + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + $field_data = $this->result_id->fetch_fields(); + for ($i = 0, $c = count($field_data); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $field_data[$i]->name; + $retval[$i]->type = $field_data[$i]->type; + $retval[$i]->max_length = $field_data[$i]->max_length; + $retval[$i]->primary_key = (int) ($field_data[$i]->flags & 2); + $retval[$i]->default = $field_data[$i]->def; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_object($this->result_id)) + { + $this->result_id->free(); + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * @param int $n + * @return bool + */ + public function data_seek($n = 0) + { + return $this->result_id->data_seek($n); + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return $this->result_id->fetch_assoc(); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return $this->result_id->fetch_object($class_name); + } + +} diff --git a/system/database/drivers/mysqli/mysqli_utility.php b/system/database/drivers/mysqli/mysqli_utility.php new file mode 100644 index 0000000..79d9f36 --- /dev/null +++ b/system/database/drivers/mysqli/mysqli_utility.php @@ -0,0 +1,213 @@ +db->query('SHOW CREATE TABLE '.$this->db->escape_identifiers($this->db->database.'.'.$table)); + + // No result means the table name was invalid + if ($query === FALSE) + { + continue; + } + + // Write out the table schema + $output .= '#'.$newline.'# TABLE STRUCTURE FOR: '.$table.$newline.'#'.$newline.$newline; + + if ($add_drop === TRUE) + { + $output .= 'DROP TABLE IF EXISTS '.$this->db->protect_identifiers($table).';'.$newline.$newline; + } + + $i = 0; + $result = $query->result_array(); + foreach ($result[0] as $val) + { + if ($i++ % 2) + { + $output .= $val.';'.$newline.$newline; + } + } + + // If inserts are not needed we're done... + if ($add_insert === FALSE) + { + continue; + } + + // Grab all the data from the current table + $query = $this->db->query('SELECT * FROM '.$this->db->protect_identifiers($table)); + + if ($query->num_rows() === 0) + { + continue; + } + + // Fetch the field names and determine if the field is an + // integer type. We use this info to decide whether to + // surround the data with quotes or not + + $i = 0; + $field_str = ''; + $is_int = array(); + while ($field = $query->result_id->fetch_field()) + { + // Most versions of MySQL store timestamp as a string + $is_int[$i] = in_array(strtolower($field->type), + array('tinyint', 'smallint', 'mediumint', 'int', 'bigint'), //, 'timestamp'), + TRUE); + + // Create a string of field names + $field_str .= $this->db->escape_identifiers($field->name).', '; + $i++; + } + + // Trim off the end comma + $field_str = preg_replace('/, $/' , '', $field_str); + + // Build the insert string + foreach ($query->result_array() as $row) + { + $val_str = ''; + + $i = 0; + foreach ($row as $v) + { + // Is the value NULL? + if ($v === NULL) + { + $val_str .= 'NULL'; + } + else + { + // Escape the data if it's not an integer + $val_str .= ($is_int[$i] === FALSE) ? $this->db->escape($v) : $v; + } + + // Append a comma + $val_str .= ', '; + $i++; + } + + // Remove the comma at the end of the string + $val_str = preg_replace('/, $/' , '', $val_str); + + // Build the INSERT string + $output .= 'INSERT INTO '.$this->db->protect_identifiers($table).' ('.$field_str.') VALUES ('.$val_str.');'.$newline; + } + + $output .= $newline.$newline; + } + + // Do we need to include a statement to re-enable foreign key checks? + if ($foreign_key_checks === FALSE) + { + $output .= 'SET foreign_key_checks = 1;'.$newline; + } + + return $output; + } + +} diff --git a/system/database/drivers/oci8/index.html b/system/database/drivers/oci8/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/oci8/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/oci8/oci8_driver.php b/system/database/drivers/oci8/oci8_driver.php new file mode 100644 index 0000000..56fdf32 --- /dev/null +++ b/system/database/drivers/oci8/oci8_driver.php @@ -0,0 +1,688 @@ + '/^\(DESCRIPTION=(\(.+\)){2,}\)$/', // TNS + // Easy Connect string (Oracle 10g+) + 'ec' => '/^(\/\/)?[a-z0-9.:_-]+(:[1-9][0-9]{0,4})?(\/[a-z0-9$_]+)?(:[^\/])?(\/[a-z0-9$_]+)?$/i', + 'in' => '/^[a-z0-9$_]+$/i' // Instance name (defined in tnsnames.ora) + ); + + /* Space characters don't have any effect when actually + * connecting, but can be a hassle while validating the DSN. + */ + $this->dsn = str_replace(array("\n", "\r", "\t", ' '), '', $this->dsn); + + if ($this->dsn !== '') + { + foreach ($valid_dsns as $regexp) + { + if (preg_match($regexp, $this->dsn)) + { + return; + } + } + } + + // Legacy support for TNS in the hostname configuration field + $this->hostname = str_replace(array("\n", "\r", "\t", ' '), '', $this->hostname); + if (preg_match($valid_dsns['tns'], $this->hostname)) + { + $this->dsn = $this->hostname; + return; + } + elseif ($this->hostname !== '' && strpos($this->hostname, '/') === FALSE && strpos($this->hostname, ':') === FALSE + && (( ! empty($this->port) && ctype_digit($this->port)) OR $this->database !== '')) + { + /* If the hostname field isn't empty, doesn't contain + * ':' and/or '/' and if port and/or database aren't + * empty, then the hostname field is most likely indeed + * just a hostname. Therefore we'll try and build an + * Easy Connect string from these 3 settings, assuming + * that the database field is a service name. + */ + $this->dsn = $this->hostname + .(( ! empty($this->port) && ctype_digit($this->port)) ? ':'.$this->port : '') + .($this->database !== '' ? '/'.ltrim($this->database, '/') : ''); + + if (preg_match($valid_dsns['ec'], $this->dsn)) + { + return; + } + } + + /* At this point, we can only try and validate the hostname and + * database fields separately as DSNs. + */ + if (preg_match($valid_dsns['ec'], $this->hostname) OR preg_match($valid_dsns['in'], $this->hostname)) + { + $this->dsn = $this->hostname; + return; + } + + $this->database = str_replace(array("\n", "\r", "\t", ' '), '', $this->database); + foreach ($valid_dsns as $regexp) + { + if (preg_match($regexp, $this->database)) + { + return; + } + } + + /* Well - OK, an empty string should work as well. + * PHP will try to use environment variables to + * determine which Oracle instance to connect to. + */ + $this->dsn = ''; + } + + // -------------------------------------------------------------------- + + /** + * Non-persistent database connection + * + * @param bool $persistent + * @return resource + */ + public function db_connect($persistent = FALSE) + { + $func = ($persistent === TRUE) ? 'oci_pconnect' : 'oci_connect'; + return empty($this->char_set) + ? $func($this->username, $this->password, $this->dsn) + : $func($this->username, $this->password, $this->dsn, $this->char_set); + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + if ( ! $this->conn_id OR ($version_string = oci_server_version($this->conn_id)) === FALSE) + { + return FALSE; + } + elseif (preg_match('#Release\s(\d+(?:\.\d+)+)#', $version_string, $match)) + { + return $this->data_cache['version'] = $match[1]; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return resource + */ + protected function _execute($sql) + { + /* Oracle must parse the query before it is run. All of the actions with + * the query are based on the statement id returned by oci_parse(). + */ + if ($this->_reset_stmt_id === TRUE) + { + $this->stmt_id = oci_parse($this->conn_id, $sql); + } + + oci_set_prefetch($this->stmt_id, 1000); + return oci_execute($this->stmt_id, $this->commit_mode); + } + + // -------------------------------------------------------------------- + + /** + * Get cursor. Returns a cursor from the database + * + * @return resource + */ + public function get_cursor() + { + return $this->curs_id = oci_new_cursor($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Stored Procedure. Executes a stored procedure + * + * @param string package name in which the stored procedure is in + * @param string stored procedure name to execute + * @param array parameters + * @return mixed + * + * params array keys + * + * KEY OPTIONAL NOTES + * name no the name of the parameter should be in : format + * value no the value of the parameter. If this is an OUT or IN OUT parameter, + * this should be a reference to a variable + * type yes the type of the parameter + * length yes the max size of the parameter + */ + public function stored_procedure($package, $procedure, array $params) + { + if ($package === '' OR $procedure === '') + { + log_message('error', 'Invalid query: '.$package.'.'.$procedure); + return ($this->db_debug) ? $this->display_error('db_invalid_query') : FALSE; + } + + // Build the query string + $sql = 'BEGIN '.$package.'.'.$procedure.'('; + + $have_cursor = FALSE; + foreach ($params as $param) + { + $sql .= $param['name'].','; + + if (isset($param['type']) && $param['type'] === OCI_B_CURSOR) + { + $have_cursor = TRUE; + } + } + $sql = trim($sql, ',').'); END;'; + + $this->_reset_stmt_id = FALSE; + $this->stmt_id = oci_parse($this->conn_id, $sql); + $this->_bind_params($params); + $result = $this->query($sql, FALSE, $have_cursor); + $this->_reset_stmt_id = TRUE; + return $result; + } + + // -------------------------------------------------------------------- + + /** + * Bind parameters + * + * @param array $params + * @return void + */ + protected function _bind_params($params) + { + if ( ! is_array($params) OR ! is_resource($this->stmt_id)) + { + return; + } + + foreach ($params as $param) + { + foreach (array('name', 'value', 'type', 'length') as $val) + { + if ( ! isset($param[$val])) + { + $param[$val] = ''; + } + } + + oci_bind_by_name($this->stmt_id, $param['name'], $param['value'], $param['length'], $param['type']); + } + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + $this->commit_mode = OCI_NO_AUTO_COMMIT; + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + $this->commit_mode = OCI_COMMIT_ON_SUCCESS; + + return oci_commit($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + $this->commit_mode = OCI_COMMIT_ON_SUCCESS; + return oci_rollback($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return oci_num_rows($this->stmt_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return int + */ + public function insert_id() + { + // not supported in oracle + return $this->display_error('db_unsupported_function'); + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "TABLE_NAME" FROM "ALL_TABLES"'; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql.' WHERE "TABLE_NAME" LIKE \''.$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + if (strpos($table, '.') !== FALSE) + { + sscanf($table, '%[^.].%s', $owner, $table); + } + else + { + $owner = $this->username; + } + + return 'SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS + WHERE UPPER(OWNER) = '.$this->escape(strtoupper($owner)).' + AND UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (strpos($table, '.') !== FALSE) + { + sscanf($table, '%[^.].%s', $owner, $table); + } + else + { + $owner = $this->username; + } + + $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHAR_LENGTH, DATA_PRECISION, DATA_LENGTH, DATA_DEFAULT, NULLABLE + FROM ALL_TAB_COLUMNS + WHERE UPPER(OWNER) = '.$this->escape(strtoupper($owner)).' + AND UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->COLUMN_NAME; + $retval[$i]->type = $query[$i]->DATA_TYPE; + + $length = ($query[$i]->CHAR_LENGTH > 0) + ? $query[$i]->CHAR_LENGTH : $query[$i]->DATA_PRECISION; + if ($length === NULL) + { + $length = $query[$i]->DATA_LENGTH; + } + $retval[$i]->max_length = $length; + + $default = $query[$i]->DATA_DEFAULT; + if ($default === NULL && $query[$i]->NULLABLE === 'N') + { + $default = ''; + } + $retval[$i]->default = $default; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occured. + * + * @return array + */ + public function error() + { + // oci_error() returns an array that already contains + // 'code' and 'message' keys, but it can return false + // if there was no error .... + if (is_resource($this->curs_id)) + { + $error = oci_error($this->curs_id); + } + elseif (is_resource($this->stmt_id)) + { + $error = oci_error($this->stmt_id); + } + elseif (is_resource($this->conn_id)) + { + $error = oci_error($this->conn_id); + } + else + { + $error = oci_error(); + } + + return is_array($error) + ? $error + : array('code' => '', 'message' => ''); + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string + */ + protected function _insert_batch($table, $keys, $values) + { + $keys = implode(', ', $keys); + $sql = "INSERT ALL\n"; + + for ($i = 0, $c = count($values); $i < $c; $i++) + { + $sql .= ' INTO '.$table.' ('.$keys.') VALUES '.$values[$i]."\n"; + } + + return $sql.'SELECT * FROM dual'; + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE TABLE '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + if ($this->qb_limit) + { + $this->where('rownum <= ',$this->qb_limit, FALSE); + $this->qb_limit = FALSE; + } + + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + if (version_compare($this->version(), '12.1', '>=')) + { + // OFFSET-FETCH can be used only with the ORDER BY clause + empty($this->qb_orderby) && $sql .= ' ORDER BY 1'; + + return $sql.' OFFSET '.(int) $this->qb_offset.' ROWS FETCH NEXT '.$this->qb_limit.' ROWS ONLY'; + } + + $this->limit_used = TRUE; + return 'SELECT * FROM (SELECT inner_query.*, rownum rnum FROM ('.$sql.') inner_query WHERE rownum < '.($this->qb_offset + $this->qb_limit + 1).')' + .($this->qb_offset ? ' WHERE rnum >= '.($this->qb_offset + 1) : ''); + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + oci_close($this->conn_id); + } + +} diff --git a/system/database/drivers/oci8/oci8_forge.php b/system/database/drivers/oci8/oci8_forge.php new file mode 100644 index 0000000..23e0257 --- /dev/null +++ b/system/database/drivers/oci8/oci8_forge.php @@ -0,0 +1,185 @@ +db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + $field[$i] = "\n\t".$field[$i]['_literal']; + } + else + { + $field[$i]['_literal'] = "\n\t".$this->_process_column($field[$i]); + + if ( ! empty($field[$i]['comment'])) + { + $sqls[] = 'COMMENT ON COLUMN ' + .$this->db->escape_identifiers($table).'.'.$this->db->escape_identifiers($field[$i]['name']) + .' IS '.$field[$i]['comment']; + } + + if ($alter_type === 'MODIFY' && ! empty($field[$i]['new_name'])) + { + $sqls[] = $sql.' RENAME COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' '.$this->db->escape_identifiers($field[$i]['new_name']); + } + } + } + + $sql .= ' '.$alter_type.' '; + $sql .= (count($field) === 1) + ? $field[0] + : '('.implode(',', $field).')'; + + // RENAME COLUMN must be executed after MODIFY + array_unshift($sqls, $sql); + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + // Not supported - sequences and triggers must be used instead + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'NUMBER'; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'NUMBER'; + return; + case 'INT': + $attributes['TYPE'] = 'NUMBER'; + return; + case 'BIGINT': + $attributes['TYPE'] = 'NUMBER'; + return; + default: return; + } + } +} diff --git a/system/database/drivers/oci8/oci8_result.php b/system/database/drivers/oci8/oci8_result.php new file mode 100644 index 0000000..fc860ea --- /dev/null +++ b/system/database/drivers/oci8/oci8_result.php @@ -0,0 +1,229 @@ +stmt_id = $driver_object->stmt_id; + $this->curs_id = $driver_object->curs_id; + $this->limit_used = $driver_object->limit_used; + $this->commit_mode =& $driver_object->commit_mode; + $driver_object->stmt_id = FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + $count = oci_num_fields($this->stmt_id); + + // if we used a limit we subtract it + return ($this->limit_used) ? $count - 1 : $count; + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + for ($c = 1, $fieldCount = $this->num_fields(); $c <= $fieldCount; $c++) + { + $field_names[] = oci_field_name($this->stmt_id, $c); + } + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + for ($c = 1, $fieldCount = $this->num_fields(); $c <= $fieldCount; $c++) + { + $F = new stdClass(); + $F->name = oci_field_name($this->stmt_id, $c); + $F->type = oci_field_type($this->stmt_id, $c); + $F->max_length = oci_field_size($this->stmt_id, $c); + + $retval[] = $F; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_resource($this->result_id)) + { + oci_free_statement($this->result_id); + $this->result_id = FALSE; + } + + if (is_resource($this->stmt_id)) + { + oci_free_statement($this->stmt_id); + } + + if (is_resource($this->curs_id)) + { + oci_cancel($this->curs_id); + $this->curs_id = NULL; + } + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + $id = ($this->curs_id) ? $this->curs_id : $this->stmt_id; + return oci_fetch_assoc($id); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + $row = ($this->curs_id) + ? oci_fetch_object($this->curs_id) + : oci_fetch_object($this->stmt_id); + + if ($class_name === 'stdClass' OR ! $row) + { + return $row; + } + + $class_name = new $class_name(); + foreach ($row as $key => $value) + { + $class_name->$key = $value; + } + + return $class_name; + } + +} diff --git a/system/database/drivers/oci8/oci8_utility.php b/system/database/drivers/oci8/oci8_utility.php new file mode 100644 index 0000000..ebe49c4 --- /dev/null +++ b/system/database/drivers/oci8/oci8_utility.php @@ -0,0 +1,68 @@ +db->display_error('db_unsupported_feature'); + } + +} diff --git a/system/database/drivers/odbc/index.html b/system/database/drivers/odbc/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/odbc/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/odbc/odbc_driver.php b/system/database/drivers/odbc/odbc_driver.php new file mode 100644 index 0000000..dbce1cf --- /dev/null +++ b/system/database/drivers/odbc/odbc_driver.php @@ -0,0 +1,425 @@ +dsn)) + { + $this->dsn = $this->hostname; + } + } + + // -------------------------------------------------------------------- + + /** + * Non-persistent database connection + * + * @param bool $persistent + * @return resource + */ + public function db_connect($persistent = FALSE) + { + return ($persistent === TRUE) + ? odbc_pconnect($this->dsn, $this->username, $this->password) + : odbc_connect($this->dsn, $this->username, $this->password); + } + + // -------------------------------------------------------------------- + + /** + * Compile Bindings + * + * @param string $sql SQL statement + * @param array $binds An array of values to bind + * @return string + */ + public function compile_binds($sql, $binds) + { + if (empty($binds) OR empty($this->bind_marker) OR strpos($sql, $this->bind_marker) === FALSE) + { + return $sql; + } + elseif ( ! is_array($binds)) + { + $binds = array($binds); + $bind_count = 1; + } + else + { + // Make sure we're using numeric keys + $binds = array_values($binds); + $bind_count = count($binds); + } + + // We'll need the marker length later + $ml = strlen($this->bind_marker); + + // Make sure not to replace a chunk inside a string that happens to match the bind marker + if ($c = preg_match_all("/'[^']*'/i", $sql, $matches)) + { + $c = preg_match_all('/'.preg_quote($this->bind_marker, '/').'/i', + str_replace($matches[0], + str_replace($this->bind_marker, str_repeat(' ', $ml), $matches[0]), + $sql, $c), + $matches, PREG_OFFSET_CAPTURE); + + // Bind values' count must match the count of markers in the query + if ($bind_count !== $c) + { + return $sql; + } + } + elseif (($c = preg_match_all('/'.preg_quote($this->bind_marker, '/').'/i', $sql, $matches, PREG_OFFSET_CAPTURE)) !== $bind_count) + { + return $sql; + } + + if ($this->bind_marker !== '?') + { + do + { + $c--; + $sql = substr_replace($sql, '?', $matches[0][$c][1], $ml); + } + while ($c !== 0); + } + + if (FALSE !== ($this->odbc_result = odbc_prepare($this->conn_id, $sql))) + { + $this->binds = array_values($binds); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return resource + */ + protected function _execute($sql) + { + if ( ! isset($this->odbc_result)) + { + return odbc_exec($this->conn_id, $sql); + } + elseif ($this->odbc_result === FALSE) + { + return FALSE; + } + + if (TRUE === ($success = odbc_execute($this->odbc_result, $this->binds))) + { + // For queries that return result sets, return the result_id resource on success + $this->is_write_type($sql) OR $success = $this->odbc_result; + } + + $this->odbc_result = NULL; + $this->binds = array(); + + return $success; + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + return odbc_autocommit($this->conn_id, FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + if (odbc_commit($this->conn_id)) + { + odbc_autocommit($this->conn_id, TRUE); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + if (odbc_rollback($this->conn_id)) + { + odbc_autocommit($this->conn_id, TRUE); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Determines if a query is a "write" type. + * + * @param string An SQL query string + * @return bool + */ + public function is_write_type($sql) + { + if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql)) + { + return FALSE; + } + + return parent::is_write_type($sql); + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependant string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + $this->db->display_error('db_unsupported_feature'); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return odbc_num_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return bool + */ + public function insert_id() + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = '".$this->schema."'"; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql." AND table_name LIKE '".$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SHOW COLUMNS FROM '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Field data query + * + * Generates a platform-specific query so that the column data can be retrieved + * + * @param string $table + * @return string + */ + protected function _field_data($table) + { + return 'SELECT TOP 1 FROM '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occured. + * + * @return array + */ + public function error() + { + return array('code' => odbc_error($this->conn_id), 'message' => odbc_errormsg($this->conn_id)); + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + odbc_close($this->conn_id); + } +} diff --git a/system/database/drivers/odbc/odbc_forge.php b/system/database/drivers/odbc/odbc_forge.php new file mode 100644 index 0000000..bac30be --- /dev/null +++ b/system/database/drivers/odbc/odbc_forge.php @@ -0,0 +1,86 @@ +num_rows)) + { + return $this->num_rows; + } + elseif (($this->num_rows = odbc_num_rows($this->result_id)) !== -1) + { + return $this->num_rows; + } + + // Work-around for ODBC subdrivers that don't support num_rows() + if (count($this->result_array) > 0) + { + return $this->num_rows = count($this->result_array); + } + elseif (count($this->result_object) > 0) + { + return $this->num_rows = count($this->result_object); + } + + return $this->num_rows = count($this->result_array()); + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return odbc_num_fields($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + $num_fields = $this->num_fields(); + + if ($num_fields > 0) + { + for ($i = 1; $i <= $num_fields; $i++) + { + $field_names[] = odbc_field_name($this->result_id, $i); + } + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + for ($i = 0, $odbc_index = 1, $c = $this->num_fields(); $i < $c; $i++, $odbc_index++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = odbc_field_name($this->result_id, $odbc_index); + $retval[$i]->type = odbc_field_type($this->result_id, $odbc_index); + $retval[$i]->max_length = odbc_field_len($this->result_id, $odbc_index); + $retval[$i]->primary_key = 0; + $retval[$i]->default = ''; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_resource($this->result_id)) + { + odbc_free_result($this->result_id); + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return odbc_fetch_array($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + $row = odbc_fetch_object($this->result_id); + + if ($class_name === 'stdClass' OR ! $row) + { + return $row; + } + + $class_name = new $class_name(); + foreach ($row as $key => $value) + { + $class_name->$key = $value; + } + + return $class_name; + } + +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('odbc_fetch_array')) +{ + /** + * ODBC Fetch array + * + * Emulates the native odbc_fetch_array() function when + * it is not available (odbc_fetch_array() requires unixODBC) + * + * @param resource &$result + * @param int $rownumber + * @return array + */ + function odbc_fetch_array(&$result, $rownumber = 1) + { + $rs = array(); + if ( ! odbc_fetch_into($result, $rs, $rownumber)) + { + return FALSE; + } + + $rs_assoc = array(); + foreach ($rs as $k => $v) + { + $field_name = odbc_field_name($result, $k+1); + $rs_assoc[$field_name] = $v; + } + + return $rs_assoc; + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('odbc_fetch_object')) +{ + /** + * ODBC Fetch object + * + * Emulates the native odbc_fetch_object() function when + * it is not available. + * + * @param resource &$result + * @param int $rownumber + * @return object + */ + function odbc_fetch_object(&$result, $rownumber = 1) + { + $rs = array(); + if ( ! odbc_fetch_into($result, $rs, $rownumber)) + { + return FALSE; + } + + $rs_object = new stdClass(); + foreach ($rs as $k => $v) + { + $field_name = odbc_field_name($result, $k+1); + $rs_object->$field_name = $v; + } + + return $rs_object; + } +} diff --git a/system/database/drivers/odbc/odbc_utility.php b/system/database/drivers/odbc/odbc_utility.php new file mode 100644 index 0000000..2e34496 --- /dev/null +++ b/system/database/drivers/odbc/odbc_utility.php @@ -0,0 +1,63 @@ +db->display_error('db_unsupported_feature'); + } + +} diff --git a/system/database/drivers/pdo/index.html b/system/database/drivers/pdo/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/pdo/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/pdo/pdo_driver.php b/system/database/drivers/pdo/pdo_driver.php new file mode 100644 index 0000000..c27607e --- /dev/null +++ b/system/database/drivers/pdo/pdo_driver.php @@ -0,0 +1,375 @@ +dsn, $match) && count($match) === 2) + { + // If there is a minimum valid dsn string pattern found, we're done + // This is for general PDO users, who tend to have a full DSN string. + $this->subdriver = $match[1]; + return; + } + // Legacy support for DSN specified in the hostname field + elseif (preg_match('/([^:]+):/', $this->hostname, $match) && count($match) === 2) + { + $this->dsn = $this->hostname; + $this->hostname = NULL; + $this->subdriver = $match[1]; + return; + } + elseif (in_array($this->subdriver, array('mssql', 'sybase'), TRUE)) + { + $this->subdriver = 'dblib'; + } + elseif ($this->subdriver === '4D') + { + $this->subdriver = '4d'; + } + elseif ( ! in_array($this->subdriver, array('4d', 'cubrid', 'dblib', 'firebird', 'ibm', 'informix', 'mysql', 'oci', 'odbc', 'pgsql', 'sqlite', 'sqlsrv'), TRUE)) + { + log_message('error', 'PDO: Invalid or non-existent subdriver'); + + if ($this->db_debug) + { + show_error('Invalid or non-existent PDO subdriver'); + } + } + + $this->dsn = NULL; + } + + // -------------------------------------------------------------------- + + /** + * Database connection + * + * @param bool $persistent + * @return object + */ + public function db_connect($persistent = FALSE) + { + if ($persistent === TRUE) + { + $this->options[PDO::ATTR_PERSISTENT] = TRUE; + } + + try + { + return new PDO($this->dsn, $this->username, $this->password, $this->options); + } + catch (PDOException $e) + { + if ($this->db_debug && empty($this->failover)) + { + $this->display_error($e->getMessage(), '', TRUE); + } + + return FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + // Not all subdrivers support the getAttribute() method + try + { + return $this->data_cache['version'] = $this->conn_id->getAttribute(PDO::ATTR_SERVER_VERSION); + } + catch (PDOException $e) + { + return parent::version(); + } + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql SQL query + * @return mixed + */ + protected function _execute($sql) + { + return $this->conn_id->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + return $this->conn_id->beginTransaction(); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + return $this->conn_id->commit(); + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + return $this->conn_id->rollBack(); + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependant string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + // Escape the string + $str = $this->conn_id->quote($str); + + // If there are duplicated quotes, trim them away + return ($str[0] === "'") + ? substr($str, 1, -1) + : $str; + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return is_object($this->result_id) ? $this->result_id->rowCount() : 0; + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @param string $name + * @return int + */ + public function insert_id($name = NULL) + { + return $this->conn_id->lastInsertId($name); + } + + // -------------------------------------------------------------------- + + /** + * Field data query + * + * Generates a platform-specific query so that the column data can be retrieved + * + * @param string $table + * @return string + */ + protected function _field_data($table) + { + return 'SELECT TOP 1 * FROM '.$this->protect_identifiers($table); + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occured. + * + * @return array + */ + public function error() + { + $error = array('code' => '00000', 'message' => ''); + $pdo_error = $this->conn_id->errorInfo(); + + if (empty($pdo_error[0])) + { + return $error; + } + + $error['code'] = isset($pdo_error[1]) ? $pdo_error[0].'/'.$pdo_error[1] : $pdo_error[0]; + if (isset($pdo_error[2])) + { + $error['message'] = $pdo_error[2]; + } + + return $error; + } + + // -------------------------------------------------------------------- + + /** + * Update_Batch statement + * + * Generates a platform-specific batch update string from the supplied data + * + * @param string $table Table name + * @param array $values Update data + * @param string $index WHERE key + * @return string + */ + protected function _update_batch($table, $values, $index) + { + $ids = array(); + foreach ($values as $key => $val) + { + $ids[] = $val[$index]; + + foreach (array_keys($val) as $field) + { + if ($field !== $index) + { + $final[$field][] = 'WHEN '.$index.' = '.$val[$index].' THEN '.$val[$field]; + } + } + } + + $cases = ''; + foreach ($final as $k => $v) + { + $cases .= $k.' = CASE '."\n"; + + foreach ($v as $row) + { + $cases .= $row."\n"; + } + + $cases .= 'ELSE '.$k.' END, '; + } + + $this->where($index.' IN('.implode(',', $ids).')', NULL, FALSE); + + return 'UPDATE '.$table.' SET '.substr($cases, 0, -2).$this->_compile_wh('qb_where'); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE TABLE '.$table; + } + +} diff --git a/system/database/drivers/pdo/pdo_forge.php b/system/database/drivers/pdo/pdo_forge.php new file mode 100644 index 0000000..2595f7b --- /dev/null +++ b/system/database/drivers/pdo/pdo_forge.php @@ -0,0 +1,65 @@ +num_rows)) + { + return $this->num_rows; + } + elseif (count($this->result_array) > 0) + { + return $this->num_rows = count($this->result_array); + } + elseif (count($this->result_object) > 0) + { + return $this->num_rows = count($this->result_object); + } + elseif (($num_rows = $this->result_id->rowCount()) > 0) + { + return $this->num_rows = $num_rows; + } + + return $this->num_rows = count($this->result_array()); + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return $this->result_id->columnCount(); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return bool + */ + public function list_fields() + { + $field_names = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + // Might trigger an E_WARNING due to not all subdrivers + // supporting getColumnMeta() + $field_names[$i] = @$this->result_id->getColumnMeta($i); + $field_names[$i] = $field_names[$i]['name']; + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + try + { + $retval = array(); + + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $field = $this->result_id->getColumnMeta($i); + + $retval[$i] = new stdClass(); + $retval[$i]->name = $field['name']; + $retval[$i]->type = $field['native_type']; + $retval[$i]->max_length = ($field['len'] > 0) ? $field['len'] : NULL; + $retval[$i]->primary_key = (int) ( ! empty($field['flags']) && in_array('primary_key', $field['flags'], TRUE)); + } + + return $retval; + } + catch (Exception $e) + { + if ($this->db->db_debug) + { + return $this->db->display_error('db_unsupported_feature'); + } + + return FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_object($this->result_id)) + { + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return $this->result_id->fetch(PDO::FETCH_ASSOC); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return $this->result_id->fetchObject($class_name); + } + +} diff --git a/system/database/drivers/pdo/pdo_utility.php b/system/database/drivers/pdo/pdo_utility.php new file mode 100644 index 0000000..384661b --- /dev/null +++ b/system/database/drivers/pdo/pdo_utility.php @@ -0,0 +1,63 @@ +db->display_error('db_unsupported_feature'); + } + +} diff --git a/system/database/drivers/pdo/subdrivers/index.html b/system/database/drivers/pdo/subdrivers/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/pdo/subdrivers/pdo_4d_driver.php b/system/database/drivers/pdo/subdrivers/pdo_4d_driver.php new file mode 100644 index 0000000..3dedfd9 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_4d_driver.php @@ -0,0 +1,200 @@ +dsn)) + { + $this->dsn = '4D:host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname); + + empty($this->port) OR $this->dsn .= ';port='.$this->port; + empty($this->database) OR $this->dsn .= ';dbname='.$this->database; + empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set; + } + elseif ( ! empty($this->char_set) && strpos($this->dsn, 'charset=', 3) === FALSE) + { + $this->dsn .= ';charset='.$this->char_set; + } + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT '.$this->escape_identifiers('TABLE_NAME').' FROM '.$this->escape_identifiers('_USER_TABLES'); + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + $sql .= ' WHERE '.$this->escape_identifiers('TABLE_NAME')." LIKE '".$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT '.$this->escape_identifiers('COLUMN_NAME').' FROM '.$this->escape_identifiers('_USER_COLUMNS') + .' WHERE '.$this->escape_identifiers('TABLE_NAME').' = '.$this->escape($table); + } + + // -------------------------------------------------------------------- + + /** + * Field data query + * + * Generates a platform-specific query so that the column data can be retrieved + * + * @param string $table + * @return string + */ + protected function _field_data($table) + { + return 'SELECT * FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE).' LIMIT 1'; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + $this->qb_limit = FALSE; + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + return $sql.' LIMIT '.$this->qb_limit.($this->qb_offset ? ' OFFSET '.$this->qb_offset : ''); + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_4d_forge.php b/system/database/drivers/pdo/subdrivers/pdo_4d_forge.php new file mode 100644 index 0000000..41994f9 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_4d_forge.php @@ -0,0 +1,217 @@ + 'INT', + 'SMALLINT' => 'INT', + 'INT' => 'INT64', + 'INT32' => 'INT64' + ); + + /** + * DEFAULT value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $_default = FALSE; + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('ADD', 'DROP'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + // No method of modifying columns is supported + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'].$field['length'] + .$field['null'] + .$field['unique'] + .$field['auto_increment']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'INTEGER': + $attributes['TYPE'] = 'INT'; + return; + case 'BIGINT': + $attributes['TYPE'] = 'INT64'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute UNIQUE + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_unique(&$attributes, &$field) + { + if ( ! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === TRUE) + { + $field['unique'] = ' UNIQUE'; + + // UNIQUE must be used with NOT NULL + $field['null'] = ' NOT NULL'; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE) + { + if (stripos($field['type'], 'int') !== FALSE) + { + $field['auto_increment'] = ' AUTO_INCREMENT'; + } + elseif (strcasecmp($field['type'], 'UUID') === 0) + { + $field['auto_increment'] = ' AUTO_GENERATE'; + } + } + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_cubrid_driver.php b/system/database/drivers/pdo/subdrivers/pdo_cubrid_driver.php new file mode 100644 index 0000000..8377798 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_cubrid_driver.php @@ -0,0 +1,250 @@ +dsn)) + { + $this->dsn = 'cubrid:host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname); + + empty($this->port) OR $this->dsn .= ';port='.$this->port; + empty($this->database) OR $this->dsn .= ';dbname='.$this->database; + empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set; + } + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SHOW TABLES'; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + return $sql." LIKE '".$this->escape_like_str($this->dbprefix)."%'"; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE))) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->Field; + + sscanf($query[$i]->Type, '%[a-z](%d)', + $retval[$i]->type, + $retval[$i]->max_length + ); + + $retval[$i]->default = $query[$i]->Default; + $retval[$i]->primary_key = (int) ($query[$i]->Key === 'PRI'); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Update_Batch statement + * + * Generates a platform-specific batch update string from the supplied data + * + * @param string $table Table name + * @param array $values Update data + * @param string $index WHERE key + * @return string + */ + protected function _update_batch($table, $values, $index) + { + $ids = array(); + foreach ($values as $key => $val) + { + $ids[] = $val[$index]; + + foreach (array_keys($val) as $field) + { + if ($field !== $index) + { + $final[$field][] = 'WHEN '.$index.' = '.$val[$index].' THEN '.$val[$field]; + } + } + } + + $cases = ''; + foreach ($final as $k => $v) + { + $cases .= $k." = CASE \n" + .implode("\n", $v)."\n" + .'ELSE '.$k.' END), '; + } + + $this->where($index.' IN('.implode(',', $ids).')', NULL, FALSE); + + return 'UPDATE '.$table.' SET '.substr($cases, 0, -2).$this->_compile_wh('qb_where'); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE '.$table; + } + + // -------------------------------------------------------------------- + + /** + * FROM tables + * + * Groups tables in FROM clauses if needed, so there is no confusion + * about operator precedence. + * + * @return string + */ + protected function _from_tables() + { + if ( ! empty($this->qb_join) && count($this->qb_from) > 1) + { + return '('.implode(', ', $this->qb_from).')'; + } + + return implode(', ', $this->qb_from); + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_cubrid_forge.php b/system/database/drivers/pdo/subdrivers/pdo_cubrid_forge.php new file mode 100644 index 0000000..b5b9507 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_cubrid_forge.php @@ -0,0 +1,230 @@ + 'INTEGER', + 'SMALLINT' => 'INTEGER', + 'INT' => 'BIGINT', + 'INTEGER' => 'BIGINT', + 'BIGINT' => 'NUMERIC', + 'FLOAT' => 'DOUBLE', + 'REAL' => 'DOUBLE' + ); + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('DROP', 'ADD'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + $sqls[] = $sql.' CHANGE '.$field[$i]['_literal']; + } + else + { + $alter_type = empty($field[$i]['new_name']) ? ' MODIFY ' : ' CHANGE '; + $sqls[] = $sql.$alter_type.$this->_process_column($field[$i]); + } + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + $extra_clause = isset($field['after']) + ? ' AFTER '.$this->db->escape_identifiers($field['after']) : ''; + + if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE) + { + $extra_clause = ' FIRST'; + } + + return $this->db->escape_identifiers($field['name']) + .(empty($field['new_name']) ? '' : ' '.$this->db->escape_identifiers($field['new_name'])) + .' '.$field['type'].$field['length'] + .$field['unsigned'] + .$field['null'] + .$field['default'] + .$field['auto_increment'] + .$field['unique'] + .$extra_clause; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'LONGTEXT': + $attributes['TYPE'] = 'STRING'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Process indexes + * + * @param string $table (ignored) + * @return string + */ + protected function _process_indexes($table) + { + $sql = ''; + + for ($i = 0, $c = count($this->keys); $i < $c; $i++) + { + if (is_array($this->keys[$i])) + { + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) + { + if ( ! isset($this->fields[$this->keys[$i][$i2]])) + { + unset($this->keys[$i][$i2]); + continue; + } + } + } + elseif ( ! isset($this->fields[$this->keys[$i]])) + { + unset($this->keys[$i]); + continue; + } + + is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]); + + $sql .= ",\n\tKEY ".$this->db->escape_identifiers(implode('_', $this->keys[$i])) + .' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).')'; + } + + $this->keys = array(); + + return $sql; + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_dblib_driver.php b/system/database/drivers/pdo/subdrivers/pdo_dblib_driver.php new file mode 100644 index 0000000..9a1cbca --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_dblib_driver.php @@ -0,0 +1,337 @@ +dsn)) + { + $this->dsn = $params['subdriver'].':host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname); + + if ( ! empty($this->port)) + { + $this->dsn .= (DIRECTORY_SEPARATOR === '\\' ? ',' : ':').$this->port; + } + + empty($this->database) OR $this->dsn .= ';dbname='.$this->database; + empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set; + empty($this->appname) OR $this->dsn .= ';appname='.$this->appname; + } + else + { + if ( ! empty($this->char_set) && strpos($this->dsn, 'charset=', 6) === FALSE) + { + $this->dsn .= ';charset='.$this->char_set; + } + + $this->subdriver = 'dblib'; + } + } + + // -------------------------------------------------------------------- + + /** + * Database connection + * + * @param bool $persistent + * @return object + */ + public function db_connect($persistent = FALSE) + { + if ($persistent === TRUE) + { + log_message('debug', "dblib driver doesn't support persistent connections"); + } + + $this->conn_id = parent::db_connect(FALSE); + + if ( ! is_object($this->conn_id)) + { + return $this->conn_id; + } + + // Determine how identifiers are escaped + $query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi'); + $query = $query->row_array(); + $this->_quoted_identifier = empty($query) ? FALSE : (bool) $query['qi']; + $this->_escape_char = ($this->_quoted_identifier) ? '"' : array('[', ']'); + + return $this->conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT '.$this->escape_identifiers('name') + .' FROM '.$this->escape_identifiers('sysobjects') + .' WHERE '.$this->escape_identifiers('type')." = 'U'"; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + $sql .= ' AND '.$this->escape_identifiers('name')." LIKE '".$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql.' ORDER BY '.$this->escape_identifiers('name'); + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->COLUMN_NAME; + $retval[$i]->type = $query[$i]->DATA_TYPE; + $retval[$i]->max_length = ($query[$i]->CHARACTER_MAXIMUM_LENGTH > 0) ? $query[$i]->CHARACTER_MAXIMUM_LENGTH : $query[$i]->NUMERIC_PRECISION; + $retval[$i]->default = $query[$i]->COLUMN_DEFAULT; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + if ($this->qb_limit) + { + return 'WITH ci_delete AS (SELECT TOP '.$this->qb_limit.' * FROM '.$table.$this->_compile_wh('qb_where').') DELETE FROM ci_delete'; + } + + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + $limit = $this->qb_offset + $this->qb_limit; + + // As of SQL Server 2005 (9.0.*) ROW_NUMBER() is supported, + // however an ORDER BY clause is required for it to work + if (version_compare($this->version(), '9', '>=') && $this->qb_offset && ! empty($this->qb_orderby)) + { + $orderby = $this->_compile_order_by(); + + // We have to strip the ORDER BY clause + $sql = trim(substr($sql, 0, strrpos($sql, $orderby))); + + // Get the fields to select from our subquery, so that we can avoid CI_rownum appearing in the actual results + if (count($this->qb_select) === 0) + { + $select = '*'; // Inevitable + } + else + { + // Use only field names and their aliases, everything else is out of our scope. + $select = array(); + $field_regexp = ($this->_quoted_identifier) + ? '("[^\"]+")' : '(\[[^\]]+\])'; + for ($i = 0, $c = count($this->qb_select); $i < $c; $i++) + { + $select[] = preg_match('/(?:\s|\.)'.$field_regexp.'$/i', $this->qb_select[$i], $m) + ? $m[1] : $this->qb_select[$i]; + } + $select = implode(', ', $select); + } + + return 'SELECT '.$select." FROM (\n\n" + .preg_replace('/^(SELECT( DISTINCT)?)/i', '\\1 ROW_NUMBER() OVER('.trim($orderby).') AS '.$this->escape_identifiers('CI_rownum').', ', $sql) + ."\n\n) ".$this->escape_identifiers('CI_subquery') + ."\nWHERE ".$this->escape_identifiers('CI_rownum').' BETWEEN '.($this->qb_offset + 1).' AND '.$limit; + } + + return preg_replace('/(^\SELECT (DISTINCT)?)/i','\\1 TOP '.$limit.' ', $sql); + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string|bool + */ + protected function _insert_batch($table, $keys, $values) + { + // Multiple-value inserts are only supported as of SQL Server 2008 + if (version_compare($this->version(), '10', '>=')) + { + return parent::_insert_batch($table, $keys, $values); + } + + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_dblib_forge.php b/system/database/drivers/pdo/subdrivers/pdo_dblib_forge.php new file mode 100644 index 0000000..8302003 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_dblib_forge.php @@ -0,0 +1,149 @@ + 'SMALLINT', + 'SMALLINT' => 'INT', + 'INT' => 'BIGINT', + 'REAL' => 'FLOAT' + ); + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('ADD', 'DROP'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' ALTER COLUMN '; + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + $sqls[] = $sql.$this->_process_column($field[$i]); + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + if (isset($attributes['CONSTRAINT']) && strpos($attributes['TYPE'], 'INT') !== FALSE) + { + unset($attributes['CONSTRAINT']); + } + + switch (strtoupper($attributes['TYPE'])) + { + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'INTEGER': + $attributes['TYPE'] = 'INT'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['auto_increment'] = ' IDENTITY(1,1)'; + } + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_firebird_driver.php b/system/database/drivers/pdo/subdrivers/pdo_firebird_driver.php new file mode 100644 index 0000000..7811d3d --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_firebird_driver.php @@ -0,0 +1,279 @@ +dsn)) + { + $this->dsn = 'firebird:'; + + if ( ! empty($this->database)) + { + $this->dsn .= 'dbname='.$this->database; + } + elseif ( ! empty($this->hostname)) + { + $this->dsn .= 'dbname='.$this->hostname; + } + + empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set; + empty($this->role) OR $this->dsn .= ';role='.$this->role; + } + elseif ( ! empty($this->char_set) && strpos($this->dsn, 'charset=', 9) === FALSE) + { + $this->dsn .= ';charset='.$this->char_set; + } + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "RDB$RELATION_NAME" FROM "RDB$RELATIONS" WHERE "RDB$RELATION_NAME" NOT LIKE \'RDB$%\' AND "RDB$RELATION_NAME" NOT LIKE \'MON$%\''; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + return $sql.' AND "RDB$RELATION_NAME" LIKE \''.$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT "RDB$FIELD_NAME" FROM "RDB$RELATION_FIELDS" WHERE "RDB$RELATION_NAME" = '.$this->escape($table); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT "rfields"."RDB$FIELD_NAME" AS "name", + CASE "fields"."RDB$FIELD_TYPE" + WHEN 7 THEN \'SMALLINT\' + WHEN 8 THEN \'INTEGER\' + WHEN 9 THEN \'QUAD\' + WHEN 10 THEN \'FLOAT\' + WHEN 11 THEN \'DFLOAT\' + WHEN 12 THEN \'DATE\' + WHEN 13 THEN \'TIME\' + WHEN 14 THEN \'CHAR\' + WHEN 16 THEN \'INT64\' + WHEN 27 THEN \'DOUBLE\' + WHEN 35 THEN \'TIMESTAMP\' + WHEN 37 THEN \'VARCHAR\' + WHEN 40 THEN \'CSTRING\' + WHEN 261 THEN \'BLOB\' + ELSE NULL + END AS "type", + "fields"."RDB$FIELD_LENGTH" AS "max_length", + "rfields"."RDB$DEFAULT_VALUE" AS "default" + FROM "RDB$RELATION_FIELDS" "rfields" + JOIN "RDB$FIELDS" "fields" ON "rfields"."RDB$FIELD_SOURCE" = "fields"."RDB$FIELD_NAME" + WHERE "rfields"."RDB$RELATION_NAME" = '.$this->escape($table).' + ORDER BY "rfields"."RDB$FIELD_POSITION"'; + + return (($query = $this->query($sql)) !== FALSE) + ? $query->result_object() + : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'DELETE FROM '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + $this->qb_limit = FALSE; + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + // Limit clause depends on if Interbase or Firebird + if (stripos($this->version(), 'firebird') !== FALSE) + { + $select = 'FIRST '.$this->qb_limit + .($this->qb_offset > 0 ? ' SKIP '.$this->qb_offset : ''); + } + else + { + $select = 'ROWS ' + .($this->qb_offset > 0 ? $this->qb_offset.' TO '.($this->qb_limit + $this->qb_offset) : $this->qb_limit); + } + + return preg_replace('`SELECT`i', 'SELECT '.$select, $sql); + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string|bool + */ + protected function _insert_batch($table, $keys, $values) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_firebird_forge.php b/system/database/drivers/pdo/subdrivers/pdo_firebird_forge.php new file mode 100644 index 0000000..50df769 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_firebird_forge.php @@ -0,0 +1,237 @@ + 'INTEGER', + 'INTEGER' => 'INT64', + 'FLOAT' => 'DOUBLE PRECISION' + ); + + /** + * NULL value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $_null = 'NULL'; + + // -------------------------------------------------------------------- + + /** + * Create database + * + * @param string $db_name + * @return string + */ + public function create_database($db_name) + { + // Firebird databases are flat files, so a path is required + + // Hostname is needed for remote access + empty($this->db->hostname) OR $db_name = $this->hostname.':'.$db_name; + + return parent::create_database('"'.$db_name.'"'); + } + + // -------------------------------------------------------------------- + + /** + * Drop database + * + * @param string $db_name (ignored) + * @return bool + */ + public function drop_database($db_name) + { + if ( ! ibase_drop_db($this->conn_id)) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + elseif ( ! empty($this->db->data_cache['db_names'])) + { + $key = array_search(strtolower($this->db->database), array_map('strtolower', $this->db->data_cache['db_names']), TRUE); + if ($key !== FALSE) + { + unset($this->db->data_cache['db_names'][$key]); + } + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('DROP', 'ADD'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + return FALSE; + } + + if (isset($field[$i]['type'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TYPE '.$field[$i]['type'].$field[$i]['length']; + } + + if ( ! empty($field[$i]['default'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' SET DEFAULT '.$field[$i]['default']; + } + + if (isset($field[$i]['null'])) + { + $sqls[] = 'UPDATE "RDB$RELATION_FIELDS" SET "RDB$NULL_FLAG" = ' + .($field[$i]['null'] === TRUE ? 'NULL' : '1') + .' WHERE "RDB$FIELD_NAME" = '.$this->db->escape($field[$i]['name']) + .' AND "RDB$RELATION_NAME" = '.$this->db->escape($table); + } + + if ( ! empty($field[$i]['new_name'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TO '.$this->db->escape_identifiers($field[$i]['new_name']); + } + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'].$field['length'] + .$field['null'] + .$field['unique'] + .$field['default']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'INT': + $attributes['TYPE'] = 'INTEGER'; + return; + case 'BIGINT': + $attributes['TYPE'] = 'INT64'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + // Not supported + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_ibm_driver.php b/system/database/drivers/pdo/subdrivers/pdo_ibm_driver.php new file mode 100644 index 0000000..2366c40 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_ibm_driver.php @@ -0,0 +1,244 @@ +dsn)) + { + $this->dsn = 'ibm:'; + + // Pre-defined DSN + if (empty($this->hostname) && empty($this->HOSTNAME) && empty($this->port) && empty($this->PORT)) + { + if (isset($this->DSN)) + { + $this->dsn .= 'DSN='.$this->DSN; + } + elseif ( ! empty($this->database)) + { + $this->dsn .= 'DSN='.$this->database; + } + + return; + } + + $this->dsn .= 'DRIVER='.(isset($this->DRIVER) ? '{'.$this->DRIVER.'}' : '{IBM DB2 ODBC DRIVER}').';'; + + if (isset($this->DATABASE)) + { + $this->dsn .= 'DATABASE='.$this->DATABASE.';'; + } + elseif ( ! empty($this->database)) + { + $this->dsn .= 'DATABASE='.$this->database.';'; + } + + if (isset($this->HOSTNAME)) + { + $this->dsn .= 'HOSTNAME='.$this->HOSTNAME.';'; + } + else + { + $this->dsn .= 'HOSTNAME='.(empty($this->hostname) ? '127.0.0.1;' : $this->hostname.';'); + } + + if (isset($this->PORT)) + { + $this->dsn .= 'PORT='.$this->port.';'; + } + elseif ( ! empty($this->port)) + { + $this->dsn .= ';PORT='.$this->port.';'; + } + + $this->dsn .= 'PROTOCOL='.(isset($this->PROTOCOL) ? $this->PROTOCOL.';' : 'TCPIP;'); + } + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "tabname" FROM "syscat"."tables" + WHERE "type" = \'T\' AND LOWER("tabschema") = '.$this->escape(strtolower($this->database)); + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + $sql .= ' AND "tabname" LIKE \''.$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return array + */ + protected function _list_columns($table = '') + { + return 'SELECT "colname" FROM "syscat"."columns" + WHERE LOWER("tabschema") = '.$this->escape(strtolower($this->database)).' + AND LOWER("tabname") = '.$this->escape(strtolower($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT "colname" AS "name", "typename" AS "type", "default" AS "default", "length" AS "max_length", + CASE "keyseq" WHEN NULL THEN 0 ELSE 1 END AS "primary_key" + FROM "syscat"."columns" + WHERE LOWER("tabschema") = '.$this->escape(strtolower($this->database)).' + AND LOWER("tabname") = '.$this->escape(strtolower($table)).' + ORDER BY "colno"'; + + return (($query = $this->query($sql)) !== FALSE) + ? $query->result_object() + : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + $this->qb_limit = FALSE; + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + $sql .= ' FETCH FIRST '.($this->qb_limit + $this->qb_offset).' ROWS ONLY'; + + return ($this->qb_offset) + ? 'SELECT * FROM ('.$sql.') WHERE rownum > '.$this->qb_offset + : $sql; + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_ibm_forge.php b/system/database/drivers/pdo/subdrivers/pdo_ibm_forge.php new file mode 100644 index 0000000..a2dbfc2 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_ibm_forge.php @@ -0,0 +1,154 @@ + 'INTEGER', + 'INT' => 'BIGINT', + 'INTEGER' => 'BIGINT' + ); + + /** + * DEFAULT value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $_default = FALSE; + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'CHANGE') + { + $alter_type = 'MODIFY'; + } + + return parent::_alter_table($alter_type, $table, $field); + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute UNIQUE + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_unique(&$attributes, &$field) + { + if ( ! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === TRUE) + { + $field['unique'] = ' UNIQUE'; + + // UNIQUE must be used with NOT NULL + $field['null'] = ' NOT NULL'; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + // Not supported + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_informix_driver.php b/system/database/drivers/pdo/subdrivers/pdo_informix_driver.php new file mode 100644 index 0000000..d40d17a --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_informix_driver.php @@ -0,0 +1,309 @@ +dsn)) + { + $this->dsn = 'informix:'; + + // Pre-defined DSN + if (empty($this->hostname) && empty($this->host) && empty($this->port) && empty($this->service)) + { + if (isset($this->DSN)) + { + $this->dsn .= 'DSN='.$this->DSN; + } + elseif ( ! empty($this->database)) + { + $this->dsn .= 'DSN='.$this->database; + } + + return; + } + + if (isset($this->host)) + { + $this->dsn .= 'host='.$this->host; + } + else + { + $this->dsn .= 'host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname); + } + + if (isset($this->service)) + { + $this->dsn .= '; service='.$this->service; + } + elseif ( ! empty($this->port)) + { + $this->dsn .= '; service='.$this->port; + } + + empty($this->database) OR $this->dsn .= '; database='.$this->database; + empty($this->server) OR $this->dsn .= '; server='.$this->server; + + $this->dsn .= '; protocol='.(isset($this->protocol) ? $this->protocol : 'onsoctcp') + .'; EnableScrollableCursors=1'; + } + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "tabname" FROM "systables" + WHERE "tabid" > 99 AND "tabtype" = \'T\' AND LOWER("owner") = '.$this->escape(strtolower($this->username)); + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + $sql .= ' AND "tabname" LIKE \''.$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + if (strpos($table, '.') !== FALSE) + { + sscanf($table, '%[^.].%s', $owner, $table); + } + else + { + $owner = $this->username; + } + + return 'SELECT "colname" FROM "systables", "syscolumns" + WHERE "systables"."tabid" = "syscolumns"."tabid" + AND "systables"."tabtype" = \'T\' + AND LOWER("systables"."owner") = '.$this->escape(strtolower($owner)).' + AND LOWER("systables"."tabname") = '.$this->escape(strtolower($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT "syscolumns"."colname" AS "name", + CASE "syscolumns"."coltype" + WHEN 0 THEN \'CHAR\' + WHEN 1 THEN \'SMALLINT\' + WHEN 2 THEN \'INTEGER\' + WHEN 3 THEN \'FLOAT\' + WHEN 4 THEN \'SMALLFLOAT\' + WHEN 5 THEN \'DECIMAL\' + WHEN 6 THEN \'SERIAL\' + WHEN 7 THEN \'DATE\' + WHEN 8 THEN \'MONEY\' + WHEN 9 THEN \'NULL\' + WHEN 10 THEN \'DATETIME\' + WHEN 11 THEN \'BYTE\' + WHEN 12 THEN \'TEXT\' + WHEN 13 THEN \'VARCHAR\' + WHEN 14 THEN \'INTERVAL\' + WHEN 15 THEN \'NCHAR\' + WHEN 16 THEN \'NVARCHAR\' + WHEN 17 THEN \'INT8\' + WHEN 18 THEN \'SERIAL8\' + WHEN 19 THEN \'SET\' + WHEN 20 THEN \'MULTISET\' + WHEN 21 THEN \'LIST\' + WHEN 22 THEN \'Unnamed ROW\' + WHEN 40 THEN \'LVARCHAR\' + WHEN 41 THEN \'BLOB/CLOB/BOOLEAN\' + WHEN 4118 THEN \'Named ROW\' + ELSE "syscolumns"."coltype" + END AS "type", + "syscolumns"."collength" as "max_length", + CASE "sysdefaults"."type" + WHEN \'L\' THEN "sysdefaults"."default" + ELSE NULL + END AS "default" + FROM "syscolumns", "systables", "sysdefaults" + WHERE "syscolumns"."tabid" = "systables"."tabid" + AND "systables"."tabid" = "sysdefaults"."tabid" + AND "syscolumns"."colno" = "sysdefaults"."colno" + AND "systables"."tabtype" = \'T\' + AND LOWER("systables"."owner") = '.$this->escape(strtolower($this->username)).' + AND LOWER("systables"."tabname") = '.$this->escape(strtolower($table)).' + ORDER BY "syscolumns"."colno"'; + + return (($query = $this->query($sql)) !== FALSE) + ? $query->result_object() + : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE TABLE ONLY '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + $this->qb_limit = FALSE; + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql $SQL Query + * @return string + */ + protected function _limit($sql) + { + $select = 'SELECT '.($this->qb_offset ? 'SKIP '.$this->qb_offset : '').'FIRST '.$this->qb_limit.' '; + return preg_replace('/^(SELECT\s)/i', $select, $sql, 1); + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_informix_forge.php b/system/database/drivers/pdo/subdrivers/pdo_informix_forge.php new file mode 100644 index 0000000..5af39b1 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_informix_forge.php @@ -0,0 +1,163 @@ + 'INTEGER', + 'INT' => 'BIGINT', + 'INTEGER' => 'BIGINT', + 'REAL' => 'DOUBLE PRECISION', + 'SMALLFLOAT' => 'DOUBLE PRECISION' + ); + + /** + * DEFAULT value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $_default = ', '; + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'CHANGE') + { + $alter_type = 'MODIFY'; + } + + return parent::_alter_table($alter_type, $table, $field); + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'BYTE': + case 'TEXT': + case 'BLOB': + case 'CLOB': + $attributes['UNIQUE'] = FALSE; + if (isset($attributes['DEFAULT'])) + { + unset($attributes['DEFAULT']); + } + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute UNIQUE + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_unique(&$attributes, &$field) + { + if ( ! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === TRUE) + { + $field['unique'] = ' UNIQUE CONSTRAINT '.$this->db->escape_identifiers($field['name']); + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + // Not supported + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_mysql_driver.php b/system/database/drivers/pdo/subdrivers/pdo_mysql_driver.php new file mode 100644 index 0000000..6452b78 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_mysql_driver.php @@ -0,0 +1,374 @@ +dsn)) + { + $this->dsn = 'mysql:host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname); + + empty($this->port) OR $this->dsn .= ';port='.$this->port; + empty($this->database) OR $this->dsn .= ';dbname='.$this->database; + empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set; + } + elseif ( ! empty($this->char_set) && strpos($this->dsn, 'charset=', 6) === FALSE) + { + $this->dsn .= ';charset='.$this->char_set; + } + } + + // -------------------------------------------------------------------- + + /** + * Database connection + * + * @param bool $persistent + * @return object + */ + public function db_connect($persistent = FALSE) + { + if (isset($this->stricton)) + { + if ($this->stricton) + { + $sql = 'CONCAT(@@sql_mode, ",", "STRICT_ALL_TABLES")'; + } + else + { + $sql = 'REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + @@sql_mode, + "STRICT_ALL_TABLES,", ""), + ",STRICT_ALL_TABLES", ""), + "STRICT_ALL_TABLES", ""), + "STRICT_TRANS_TABLES,", ""), + ",STRICT_TRANS_TABLES", ""), + "STRICT_TRANS_TABLES", "")'; + } + + if ( ! empty($sql)) + { + if (empty($this->options[PDO::MYSQL_ATTR_INIT_COMMAND])) + { + $this->options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET SESSION sql_mode = '.$sql; + } + else + { + $this->options[PDO::MYSQL_ATTR_INIT_COMMAND] .= ', @@session.sql_mode = '.$sql; + } + } + } + + if ($this->compress === TRUE) + { + $this->options[PDO::MYSQL_ATTR_COMPRESS] = TRUE; + } + + if (is_array($this->encrypt)) + { + $ssl = array(); + empty($this->encrypt['ssl_key']) OR $ssl[PDO::MYSQL_ATTR_SSL_KEY] = $this->encrypt['ssl_key']; + empty($this->encrypt['ssl_cert']) OR $ssl[PDO::MYSQL_ATTR_SSL_CERT] = $this->encrypt['ssl_cert']; + empty($this->encrypt['ssl_ca']) OR $ssl[PDO::MYSQL_ATTR_SSL_CA] = $this->encrypt['ssl_ca']; + empty($this->encrypt['ssl_capath']) OR $ssl[PDO::MYSQL_ATTR_SSL_CAPATH] = $this->encrypt['ssl_capath']; + empty($this->encrypt['ssl_cipher']) OR $ssl[PDO::MYSQL_ATTR_SSL_CIPHER] = $this->encrypt['ssl_cipher']; + + // DO NOT use array_merge() here! + // It re-indexes numeric keys and the PDO_MYSQL_ATTR_SSL_* constants are integers. + empty($ssl) OR $this->options += $ssl; + } + + // Prior to version 5.7.3, MySQL silently downgrades to an unencrypted connection if SSL setup fails + if ( + ($pdo = parent::db_connect($persistent)) !== FALSE + && ! empty($ssl) + && version_compare($pdo->getAttribute(PDO::ATTR_CLIENT_VERSION), '5.7.3', '<=') + && empty($pdo->query("SHOW STATUS LIKE 'ssl_cipher'")->fetchObject()->Value) + ) + { + $message = 'PDO_MYSQL was configured for an SSL connection, but got an unencrypted connection instead!'; + log_message('error', $message); + return ($this->db->db_debug) ? $this->db->display_error($message, '', TRUE) : FALSE; + } + + return $pdo; + } + + // -------------------------------------------------------------------- + + /** + * Select the database + * + * @param string $database + * @return bool + */ + public function db_select($database = '') + { + if ($database === '') + { + $database = $this->database; + } + + if (FALSE !== $this->simple_query('USE '.$this->escape_identifiers($database))) + { + $this->database = $database; + $this->data_cache = array(); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + $this->conn_id->setAttribute(PDO::ATTR_AUTOCOMMIT, FALSE); + return $this->conn_id->beginTransaction(); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + if ($this->conn_id->commit()) + { + $this->conn_id->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + if ($this->conn_id->rollBack()) + { + $this->conn_id->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SHOW TABLES'; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + return $sql." LIKE '".$this->escape_like_str($this->dbprefix)."%'"; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE))) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->Field; + + sscanf($query[$i]->Type, '%[a-z](%d)', + $retval[$i]->type, + $retval[$i]->max_length + ); + + $retval[$i]->default = $query[$i]->Default; + $retval[$i]->primary_key = (int) ($query[$i]->Key === 'PRI'); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE '.$table; + } + + // -------------------------------------------------------------------- + + /** + * FROM tables + * + * Groups tables in FROM clauses if needed, so there is no confusion + * about operator precedence. + * + * @return string + */ + protected function _from_tables() + { + if ( ! empty($this->qb_join) && count($this->qb_from) > 1) + { + return '('.implode(', ', $this->qb_from).')'; + } + + return implode(', ', $this->qb_from); + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_mysql_forge.php b/system/database/drivers/pdo/subdrivers/pdo_mysql_forge.php new file mode 100644 index 0000000..9d04a8a --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_mysql_forge.php @@ -0,0 +1,256 @@ +db->char_set) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET')) + { + $sql .= ' DEFAULT CHARACTER SET = '.$this->db->char_set; + } + + if ( ! empty($this->db->dbcollat) && ! strpos($sql, 'COLLATE')) + { + $sql .= ' COLLATE = '.$this->db->dbcollat; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'DROP') + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + $field[$i] = ($alter_type === 'ADD') + ? "\n\tADD ".$field[$i]['_literal'] + : "\n\tMODIFY ".$field[$i]['_literal']; + } + else + { + if ($alter_type === 'ADD') + { + $field[$i]['_literal'] = "\n\tADD "; + } + else + { + $field[$i]['_literal'] = empty($field[$i]['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE "; + } + + $field[$i] = $field[$i]['_literal'].$this->_process_column($field[$i]); + } + } + + return array($sql.implode(',', $field)); + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + $extra_clause = isset($field['after']) + ? ' AFTER '.$this->db->escape_identifiers($field['after']) : ''; + + if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE) + { + $extra_clause = ' FIRST'; + } + + return $this->db->escape_identifiers($field['name']) + .(empty($field['new_name']) ? '' : ' '.$this->db->escape_identifiers($field['new_name'])) + .' '.$field['type'].$field['length'] + .$field['unsigned'] + .$field['null'] + .$field['default'] + .$field['auto_increment'] + .$field['unique'] + .(empty($field['comment']) ? '' : ' COMMENT '.$field['comment']) + .$extra_clause; + } + + // -------------------------------------------------------------------- + + /** + * Process indexes + * + * @param string $table (ignored) + * @return string + */ + protected function _process_indexes($table) + { + $sql = ''; + + for ($i = 0, $c = count($this->keys); $i < $c; $i++) + { + if (is_array($this->keys[$i])) + { + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) + { + if ( ! isset($this->fields[$this->keys[$i][$i2]])) + { + unset($this->keys[$i][$i2]); + continue; + } + } + } + elseif ( ! isset($this->fields[$this->keys[$i]])) + { + unset($this->keys[$i]); + continue; + } + + is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]); + + $sql .= ",\n\tKEY ".$this->db->escape_identifiers(implode('_', $this->keys[$i])) + .' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).')'; + } + + $this->keys = array(); + + return $sql; + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_oci_driver.php b/system/database/drivers/pdo/subdrivers/pdo_oci_driver.php new file mode 100644 index 0000000..dd1d31c --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_oci_driver.php @@ -0,0 +1,326 @@ +dsn)) + { + $this->dsn = 'oci:dbname='; + + // Oracle has a slightly different PDO DSN format (Easy Connect), + // which also supports pre-defined DSNs. + if (empty($this->hostname) && empty($this->port)) + { + $this->dsn .= $this->database; + } + else + { + $this->dsn .= '//'.(empty($this->hostname) ? '127.0.0.1' : $this->hostname) + .(empty($this->port) ? '' : ':'.$this->port).'/'; + + empty($this->database) OR $this->dsn .= $this->database; + } + + empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set; + } + elseif ( ! empty($this->char_set) && strpos($this->dsn, 'charset=', 4) === FALSE) + { + $this->dsn .= ';charset='.$this->char_set; + } + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + $version_string = parent::version(); + if (preg_match('#Release\s(?\d+(?:\.\d+)+)#', $version_string, $match)) + { + return $this->data_cache['version'] = $match[1]; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "TABLE_NAME" FROM "ALL_TABLES"'; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + return $sql.' WHERE "TABLE_NAME" LIKE \''.$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + if (strpos($table, '.') !== FALSE) + { + sscanf($table, '%[^.].%s', $owner, $table); + } + else + { + $owner = $this->username; + } + + return 'SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS + WHERE UPPER(OWNER) = '.$this->escape(strtoupper($owner)).' + AND UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (strpos($table, '.') !== FALSE) + { + sscanf($table, '%[^.].%s', $owner, $table); + } + else + { + $owner = $this->username; + } + + $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHAR_LENGTH, DATA_PRECISION, DATA_LENGTH, DATA_DEFAULT, NULLABLE + FROM ALL_TAB_COLUMNS + WHERE UPPER(OWNER) = '.$this->escape(strtoupper($owner)).' + AND UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->COLUMN_NAME; + $retval[$i]->type = $query[$i]->DATA_TYPE; + + $length = ($query[$i]->CHAR_LENGTH > 0) + ? $query[$i]->CHAR_LENGTH : $query[$i]->DATA_PRECISION; + if ($length === NULL) + { + $length = $query[$i]->DATA_LENGTH; + } + $retval[$i]->max_length = $length; + + $default = $query[$i]->DATA_DEFAULT; + if ($default === NULL && $query[$i]->NULLABLE === 'N') + { + $default = ''; + } + $retval[$i]->default = $query[$i]->COLUMN_DEFAULT; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string + */ + protected function _insert_batch($table, $keys, $values) + { + $keys = implode(', ', $keys); + $sql = "INSERT ALL\n"; + + for ($i = 0, $c = count($values); $i < $c; $i++) + { + $sql .= ' INTO '.$table.' ('.$keys.') VALUES '.$values[$i]."\n"; + } + + return $sql.'SELECT * FROM dual'; + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + if ($this->qb_limit) + { + $this->where('rownum <= ',$this->qb_limit, FALSE); + $this->qb_limit = FALSE; + } + + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + if (version_compare($this->version(), '12.1', '>=')) + { + // OFFSET-FETCH can be used only with the ORDER BY clause + empty($this->qb_orderby) && $sql .= ' ORDER BY 1'; + + return $sql.' OFFSET '.(int) $this->qb_offset.' ROWS FETCH NEXT '.$this->qb_limit.' ROWS ONLY'; + } + + return 'SELECT * FROM (SELECT inner_query.*, rownum rnum FROM ('.$sql.') inner_query WHERE rownum < '.($this->qb_offset + $this->qb_limit + 1).')' + .($this->qb_offset ? ' WHERE rnum >= '.($this->qb_offset + 1): ''); + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_oci_forge.php b/system/database/drivers/pdo/subdrivers/pdo_oci_forge.php new file mode 100644 index 0000000..705b1c7 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_oci_forge.php @@ -0,0 +1,176 @@ +db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + $field[$i] = "\n\t".$field[$i]['_literal']; + } + else + { + $field[$i]['_literal'] = "\n\t".$this->_process_column($field[$i]); + + if ( ! empty($field[$i]['comment'])) + { + $sqls[] = 'COMMENT ON COLUMN ' + .$this->db->escape_identifiers($table).'.'.$this->db->escape_identifiers($field[$i]['name']) + .' IS '.$field[$i]['comment']; + } + + if ($alter_type === 'MODIFY' && ! empty($field[$i]['new_name'])) + { + $sqls[] = $sql.' RENAME COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' '.$this->db->escape_identifiers($field[$i]['new_name']); + } + } + } + + $sql .= ' '.$alter_type.' '; + $sql .= (count($field) === 1) + ? $field[0] + : '('.implode(',', $field).')'; + + // RENAME COLUMN must be executed after MODIFY + array_unshift($sqls, $sql); + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + // Not supported - sequences and triggers must be used instead + } + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'NUMBER'; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'NUMBER'; + return; + case 'INT': + $attributes['TYPE'] = 'NUMBER'; + return; + case 'BIGINT': + $attributes['TYPE'] = 'NUMBER'; + return; + default: return; + } + } +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_odbc_driver.php b/system/database/drivers/pdo/subdrivers/pdo_odbc_driver.php new file mode 100644 index 0000000..ebe1ed6 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_odbc_driver.php @@ -0,0 +1,229 @@ +dsn)) + { + $this->dsn = 'odbc:'; + + // Pre-defined DSN + if (empty($this->hostname) && empty($this->HOSTNAME) && empty($this->port) && empty($this->PORT)) + { + if (isset($this->DSN)) + { + $this->dsn .= 'DSN='.$this->DSN; + } + elseif ( ! empty($this->database)) + { + $this->dsn .= 'DSN='.$this->database; + } + + return; + } + + // If the DSN is not pre-configured - try to build an IBM DB2 connection string + $this->dsn .= 'DRIVER='.(isset($this->DRIVER) ? '{'.$this->DRIVER.'}' : '{IBM DB2 ODBC DRIVER}').';'; + + if (isset($this->DATABASE)) + { + $this->dsn .= 'DATABASE='.$this->DATABASE.';'; + } + elseif ( ! empty($this->database)) + { + $this->dsn .= 'DATABASE='.$this->database.';'; + } + + if (isset($this->HOSTNAME)) + { + $this->dsn .= 'HOSTNAME='.$this->HOSTNAME.';'; + } + else + { + $this->dsn .= 'HOSTNAME='.(empty($this->hostname) ? '127.0.0.1;' : $this->hostname.';'); + } + + if (isset($this->PORT)) + { + $this->dsn .= 'PORT='.$this->port.';'; + } + elseif ( ! empty($this->port)) + { + $this->dsn .= ';PORT='.$this->port.';'; + } + + $this->dsn .= 'PROTOCOL='.(isset($this->PROTOCOL) ? $this->PROTOCOL.';' : 'TCPIP;'); + } + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependant string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + $this->db->display_error('db_unsupported_feature'); + } + + // -------------------------------------------------------------------- + + /** + * Determines if a query is a "write" type. + * + * @param string An SQL query string + * @return bool + */ + public function is_write_type($sql) + { + if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql)) + { + return FALSE; + } + + return parent::is_write_type($sql); + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = '".$this->schema."'"; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql." AND table_name LIKE '".$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT column_name FROM information_schema.columns WHERE table_name = '.$this->escape($table); + } +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_odbc_forge.php b/system/database/drivers/pdo/subdrivers/pdo_odbc_forge.php new file mode 100644 index 0000000..7c65daa --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_odbc_forge.php @@ -0,0 +1,70 @@ +dsn)) + { + $this->dsn = 'pgsql:host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname); + + empty($this->port) OR $this->dsn .= ';port='.$this->port; + empty($this->database) OR $this->dsn .= ';dbname='.$this->database; + + if ( ! empty($this->username)) + { + $this->dsn .= ';username='.$this->username; + empty($this->password) OR $this->dsn .= ';password='.$this->password; + } + } + } + + // -------------------------------------------------------------------- + + /** + * Database connection + * + * @param bool $persistent + * @return object + */ + public function db_connect($persistent = FALSE) + { + $this->conn_id = parent::db_connect($persistent); + + if (is_object($this->conn_id) && ! empty($this->schema)) + { + $this->simple_query('SET search_path TO '.$this->schema.',public'); + } + + return $this->conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @param string $name + * @return int + */ + public function insert_id($name = NULL) + { + if ($name === NULL && version_compare($this->version(), '8.1', '>=')) + { + $query = $this->query('SELECT LASTVAL() AS ins_id'); + $query = $query->row(); + return $query->ins_id; + } + + return $this->conn_id->lastInsertId($name); + } + + // -------------------------------------------------------------------- + + /** + * Determines if a query is a "write" type. + * + * @param string An SQL query string + * @return bool + */ + public function is_write_type($sql) + { + if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql)) + { + return FALSE; + } + + return parent::is_write_type($sql); + } + + // -------------------------------------------------------------------- + + /** + * "Smart" Escape String + * + * Escapes data based on type + * + * @param string $str + * @return mixed + */ + public function escape($str) + { + if (is_bool($str)) + { + return ($str) ? 'TRUE' : 'FALSE'; + } + + return parent::escape($str); + } + + // -------------------------------------------------------------------- + + /** + * ORDER BY + * + * @param string $orderby + * @param string $direction ASC, DESC or RANDOM + * @param bool $escape + * @return object + */ + public function order_by($orderby, $direction = '', $escape = NULL) + { + $direction = strtoupper(trim($direction)); + if ($direction === 'RANDOM') + { + if ( ! is_float($orderby) && ctype_digit((string) $orderby)) + { + $orderby = ($orderby > 1) + ? (float) '0.'.$orderby + : (float) $orderby; + } + + if (is_float($orderby)) + { + $this->simple_query('SET SEED '.$orderby); + } + + $orderby = $this->_random_keyword[0]; + $direction = ''; + $escape = FALSE; + } + + return parent::order_by($orderby, $direction, $escape); + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = \''.$this->schema."'"; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + return $sql.' AND "table_name" LIKE \'' + .$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * List column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT "column_name" + FROM "information_schema"."columns" + WHERE LOWER("table_name") = '.$this->escape(strtolower($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default" + FROM "information_schema"."columns" + WHERE LOWER("table_name") = '.$this->escape(strtolower($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->column_name; + $retval[$i]->type = $query[$i]->data_type; + $retval[$i]->max_length = ($query[$i]->character_maximum_length > 0) ? $query[$i]->character_maximum_length : $query[$i]->numeric_precision; + $retval[$i]->default = $query[$i]->column_default; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Update_Batch statement + * + * Generates a platform-specific batch update string from the supplied data + * + * @param string $table Table name + * @param array $values Update data + * @param string $index WHERE key + * @return string + */ + protected function _update_batch($table, $values, $index) + { + $ids = array(); + foreach ($values as $key => $val) + { + $ids[] = $val[$index]; + + foreach (array_keys($val) as $field) + { + if ($field !== $index) + { + $final[$field][] = 'WHEN '.$val[$index].' THEN '.$val[$field]; + } + } + } + + $cases = ''; + foreach ($final as $k => $v) + { + $cases .= $k.' = (CASE '.$index."\n" + .implode("\n", $v)."\n" + .'ELSE '.$k.' END), '; + } + + $this->where($index.' IN('.implode(',', $ids).')', NULL, FALSE); + + return 'UPDATE '.$table.' SET '.substr($cases, 0, -2).$this->_compile_wh('qb_where'); + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + $this->qb_limit = FALSE; + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + return $sql.' LIMIT '.$this->qb_limit.($this->qb_offset ? ' OFFSET '.$this->qb_offset : ''); + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_pgsql_forge.php b/system/database/drivers/pdo/subdrivers/pdo_pgsql_forge.php new file mode 100644 index 0000000..214b6f5 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_pgsql_forge.php @@ -0,0 +1,210 @@ + 'INTEGER', + 'SMALLINT' => 'INTEGER', + 'INT' => 'BIGINT', + 'INT4' => 'BIGINT', + 'INTEGER' => 'BIGINT', + 'INT8' => 'NUMERIC', + 'BIGINT' => 'NUMERIC', + 'REAL' => 'DOUBLE PRECISION', + 'FLOAT' => 'DOUBLE PRECISION' + ); + + /** + * NULL value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $_null = 'NULL'; + + // -------------------------------------------------------------------- + + /** + * Class constructor + * + * @param object &$db Database object + * @return void + */ + public function __construct(&$db) + { + parent::__construct($db); + + if (version_compare($this->db->version(), '9.0', '>')) + { + $this->create_table_if = 'CREATE TABLE IF NOT EXISTS'; + } + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('DROP', 'ADD'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + return FALSE; + } + + if (version_compare($this->db->version(), '8', '>=') && isset($field[$i]['type'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TYPE '.$field[$i]['type'].$field[$i]['length']; + } + + if ( ! empty($field[$i]['default'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' SET DEFAULT '.$field[$i]['default']; + } + + if (isset($field[$i]['null'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .($field[$i]['null'] === TRUE ? ' DROP NOT NULL' : ' SET NOT NULL'); + } + + if ( ! empty($field[$i]['new_name'])) + { + $sqls[] = $sql.' RENAME COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TO '.$this->db->escape_identifiers($field[$i]['new_name']); + } + + if ( ! empty($field[$i]['comment'])) + { + $sqls[] = 'COMMENT ON COLUMN ' + .$this->db->escape_identifiers($table).'.'.$this->db->escape_identifiers($field[$i]['name']) + .' IS '.$field[$i]['comment']; + } + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + // Reset field lenghts for data types that don't support it + if (isset($attributes['CONSTRAINT']) && stripos($attributes['TYPE'], 'int') !== FALSE) + { + $attributes['CONSTRAINT'] = NULL; + } + + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE) + { + $field['type'] = ($field['type'] === 'NUMERIC') + ? 'BIGSERIAL' + : 'SERIAL'; + } + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_sqlite_driver.php b/system/database/drivers/pdo/subdrivers/pdo_sqlite_driver.php new file mode 100644 index 0000000..6269013 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_sqlite_driver.php @@ -0,0 +1,219 @@ +dsn)) + { + $this->dsn = 'sqlite:'; + + if (empty($this->database) && empty($this->hostname)) + { + $this->database = ':memory:'; + } + + $this->database = empty($this->database) ? $this->hostname : $this->database; + } + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + return $sql.' AND "NAME" LIKE \''.$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * @param string $table Table name + * @return array + */ + public function list_fields($table) + { + // Is there a cached result? + if (isset($this->data_cache['field_names'][$table])) + { + return $this->data_cache['field_names'][$table]; + } + + if (($result = $this->query('PRAGMA TABLE_INFO('.$this->protect_identifiers($table, TRUE, NULL, FALSE).')')) === FALSE) + { + return FALSE; + } + + $this->data_cache['field_names'][$table] = array(); + foreach ($result->result_array() as $row) + { + $this->data_cache['field_names'][$table][] = $row['name']; + } + + return $this->data_cache['field_names'][$table]; + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('PRAGMA TABLE_INFO('.$this->protect_identifiers($table, TRUE, NULL, FALSE).')')) === FALSE) + { + return FALSE; + } + + $query = $query->result_array(); + if (empty($query)) + { + return FALSE; + } + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]['name']; + $retval[$i]->type = $query[$i]['type']; + $retval[$i]->max_length = NULL; + $retval[$i]->default = $query[$i]['dflt_value']; + $retval[$i]->primary_key = isset($query[$i]['pk']) ? (int) $query[$i]['pk'] : 0; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Replace statement + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string + */ + protected function _replace($table, $keys, $values) + { + return 'INSERT OR '.parent::_replace($table, $keys, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'DELETE FROM '.$table; + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_sqlite_forge.php b/system/database/drivers/pdo/subdrivers/pdo_sqlite_forge.php new file mode 100644 index 0000000..b124bca --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_sqlite_forge.php @@ -0,0 +1,238 @@ +db->version(), '3.3', '<')) + { + $this->_create_table_if = FALSE; + $this->_drop_table_if = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Create database + * + * @param string $db_name (ignored) + * @return bool + */ + public function create_database($db_name) + { + // In SQLite, a database is created when you connect to the database. + // We'll return TRUE so that an error isn't generated + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Drop database + * + * @param string $db_name (ignored) + * @return bool + */ + public function drop_database($db_name) + { + // In SQLite, a database is dropped when we delete a file + if (file_exists($this->db->database)) + { + // We need to close the pseudo-connection first + $this->db->close(); + if ( ! @unlink($this->db->database)) + { + return $this->db->db_debug ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + elseif ( ! empty($this->db->data_cache['db_names'])) + { + $key = array_search(strtolower($this->db->database), array_map('strtolower', $this->db->data_cache['db_names']), TRUE); + if ($key !== FALSE) + { + unset($this->db->data_cache['db_names'][$key]); + } + } + + return TRUE; + } + + return $this->db->db_debug ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'DROP' OR $alter_type === 'CHANGE') + { + // drop_column(): + // BEGIN TRANSACTION; + // CREATE TEMPORARY TABLE t1_backup(a,b); + // INSERT INTO t1_backup SELECT a,b FROM t1; + // DROP TABLE t1; + // CREATE TABLE t1(a,b); + // INSERT INTO t1 SELECT a,b FROM t1_backup; + // DROP TABLE t1_backup; + // COMMIT; + + return FALSE; + } + + return parent::_alter_table($alter_type, $table, $field); + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'] + .$field['auto_increment'] + .$field['null'] + .$field['unique'] + .$field['default']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'ENUM': + case 'SET': + $attributes['TYPE'] = 'TEXT'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['type'] = 'INTEGER PRIMARY KEY'; + $field['default'] = ''; + $field['null'] = ''; + $field['unique'] = ''; + $field['auto_increment'] = ' AUTOINCREMENT'; + + $this->primary_keys = array(); + } + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_driver.php b/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_driver.php new file mode 100644 index 0000000..dfccb7d --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_driver.php @@ -0,0 +1,369 @@ +dsn)) + { + $this->dsn = 'sqlsrv:Server='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname); + + empty($this->port) OR $this->dsn .= ','.$this->port; + empty($this->database) OR $this->dsn .= ';Database='.$this->database; + + // Some custom options + + if (isset($this->QuotedId)) + { + $this->dsn .= ';QuotedId='.$this->QuotedId; + $this->_quoted_identifier = (bool) $this->QuotedId; + } + + if (isset($this->ConnectionPooling)) + { + $this->dsn .= ';ConnectionPooling='.$this->ConnectionPooling; + } + + if ($this->encrypt === TRUE) + { + $this->dsn .= ';Encrypt=1'; + } + + if (isset($this->TraceOn)) + { + $this->dsn .= ';TraceOn='.$this->TraceOn; + } + + if (isset($this->TrustServerCertificate)) + { + $this->dsn .= ';TrustServerCertificate='.$this->TrustServerCertificate; + } + + empty($this->APP) OR $this->dsn .= ';APP='.$this->APP; + empty($this->Failover_Partner) OR $this->dsn .= ';Failover_Partner='.$this->Failover_Partner; + empty($this->LoginTimeout) OR $this->dsn .= ';LoginTimeout='.$this->LoginTimeout; + empty($this->MultipleActiveResultSets) OR $this->dsn .= ';MultipleActiveResultSets='.$this->MultipleActiveResultSets; + empty($this->TraceFile) OR $this->dsn .= ';TraceFile='.$this->TraceFile; + empty($this->WSID) OR $this->dsn .= ';WSID='.$this->WSID; + } + elseif (preg_match('/QuotedId=(0|1)/', $this->dsn, $match)) + { + $this->_quoted_identifier = (bool) $match[1]; + } + } + + // -------------------------------------------------------------------- + + /** + * Database connection + * + * @param bool $persistent + * @return object + */ + public function db_connect($persistent = FALSE) + { + if ( ! empty($this->char_set) && preg_match('/utf[^8]*8/i', $this->char_set)) + { + $this->options[PDO::SQLSRV_ENCODING_UTF8] = 1; + } + + $this->conn_id = parent::db_connect($persistent); + + if ( ! is_object($this->conn_id) OR is_bool($this->_quoted_identifier)) + { + return $this->conn_id; + } + + // Determine how identifiers are escaped + $query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi'); + $query = $query->row_array(); + $this->_quoted_identifier = empty($query) ? FALSE : (bool) $query['qi']; + $this->_escape_char = ($this->_quoted_identifier) ? '"' : array('[', ']'); + + return $this->conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT '.$this->escape_identifiers('name') + .' FROM '.$this->escape_identifiers('sysobjects') + .' WHERE '.$this->escape_identifiers('type')." = 'U'"; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + $sql .= ' AND '.$this->escape_identifiers('name')." LIKE '".$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql.' ORDER BY '.$this->escape_identifiers('name'); + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->COLUMN_NAME; + $retval[$i]->type = $query[$i]->DATA_TYPE; + $retval[$i]->max_length = ($query[$i]->CHARACTER_MAXIMUM_LENGTH > 0) ? $query[$i]->CHARACTER_MAXIMUM_LENGTH : $query[$i]->NUMERIC_PRECISION; + $retval[$i]->default = $query[$i]->COLUMN_DEFAULT; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + if ($this->qb_limit) + { + return 'WITH ci_delete AS (SELECT TOP '.$this->qb_limit.' * FROM '.$table.$this->_compile_wh('qb_where').') DELETE FROM ci_delete'; + } + + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + // As of SQL Server 2012 (11.0.*) OFFSET is supported + if (version_compare($this->version(), '11', '>=')) + { + // SQL Server OFFSET-FETCH can be used only with the ORDER BY clause + empty($this->qb_orderby) && $sql .= ' ORDER BY 1'; + + return $sql.' OFFSET '.(int) $this->qb_offset.' ROWS FETCH NEXT '.$this->qb_limit.' ROWS ONLY'; + } + + $limit = $this->qb_offset + $this->qb_limit; + + // An ORDER BY clause is required for ROW_NUMBER() to work + if ($this->qb_offset && ! empty($this->qb_orderby)) + { + $orderby = $this->_compile_order_by(); + + // We have to strip the ORDER BY clause + $sql = trim(substr($sql, 0, strrpos($sql, $orderby))); + + // Get the fields to select from our subquery, so that we can avoid CI_rownum appearing in the actual results + if (count($this->qb_select) === 0) + { + $select = '*'; // Inevitable + } + else + { + // Use only field names and their aliases, everything else is out of our scope. + $select = array(); + $field_regexp = ($this->_quoted_identifier) + ? '("[^\"]+")' : '(\[[^\]]+\])'; + for ($i = 0, $c = count($this->qb_select); $i < $c; $i++) + { + $select[] = preg_match('/(?:\s|\.)'.$field_regexp.'$/i', $this->qb_select[$i], $m) + ? $m[1] : $this->qb_select[$i]; + } + $select = implode(', ', $select); + } + + return 'SELECT '.$select." FROM (\n\n" + .preg_replace('/^(SELECT( DISTINCT)?)/i', '\\1 ROW_NUMBER() OVER('.trim($orderby).') AS '.$this->escape_identifiers('CI_rownum').', ', $sql) + ."\n\n) ".$this->escape_identifiers('CI_subquery') + ."\nWHERE ".$this->escape_identifiers('CI_rownum').' BETWEEN '.($this->qb_offset + 1).' AND '.$limit; + } + + return preg_replace('/(^\SELECT (DISTINCT)?)/i','\\1 TOP '.$limit.' ', $sql); + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string|bool + */ + protected function _insert_batch($table, $keys, $values) + { + // Multiple-value inserts are only supported as of SQL Server 2008 + if (version_compare($this->version(), '10', '>=')) + { + return parent::_insert_batch($table, $keys, $values); + } + + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_forge.php b/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_forge.php new file mode 100644 index 0000000..56bf87f --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_forge.php @@ -0,0 +1,149 @@ + 'SMALLINT', + 'SMALLINT' => 'INT', + 'INT' => 'BIGINT', + 'REAL' => 'FLOAT' + ); + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('ADD', 'DROP'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' ALTER COLUMN '; + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + $sqls[] = $sql.$this->_process_column($field[$i]); + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + if (isset($attributes['CONSTRAINT']) && strpos($attributes['TYPE'], 'INT') !== FALSE) + { + unset($attributes['CONSTRAINT']); + } + + switch (strtoupper($attributes['TYPE'])) + { + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'INTEGER': + $attributes['TYPE'] = 'INT'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['auto_increment'] = ' IDENTITY(1,1)'; + } + } + +} diff --git a/system/database/drivers/postgre/index.html b/system/database/drivers/postgre/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/postgre/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/postgre/postgre_driver.php b/system/database/drivers/postgre/postgre_driver.php new file mode 100644 index 0000000..dfd87f9 --- /dev/null +++ b/system/database/drivers/postgre/postgre_driver.php @@ -0,0 +1,620 @@ +dsn)) + { + return; + } + + $this->dsn === '' OR $this->dsn = ''; + + if (strpos($this->hostname, '/') !== FALSE) + { + // If UNIX sockets are used, we shouldn't set a port + $this->port = ''; + } + + $this->hostname === '' OR $this->dsn = 'host='.$this->hostname.' '; + + if ( ! empty($this->port) && ctype_digit($this->port)) + { + $this->dsn .= 'port='.$this->port.' '; + } + + if ($this->username !== '') + { + $this->dsn .= 'user='.$this->username.' '; + + /* An empty password is valid! + * + * $db['password'] = NULL must be done in order to ignore it. + */ + $this->password === NULL OR $this->dsn .= "password='".$this->password."' "; + } + + $this->database === '' OR $this->dsn .= 'dbname='.$this->database.' '; + + /* We don't have these options as elements in our standard configuration + * array, but they might be set by parse_url() if the configuration was + * provided via string. Example: + * + * postgre://username:password@localhost:5432/database?connect_timeout=5&sslmode=1 + */ + foreach (array('connect_timeout', 'options', 'sslmode', 'service') as $key) + { + if (isset($this->$key) && is_string($this->key) && $this->key !== '') + { + $this->dsn .= $key."='".$this->key."' "; + } + } + + $this->dsn = rtrim($this->dsn); + } + + // -------------------------------------------------------------------- + + /** + * Database connection + * + * @param bool $persistent + * @return resource + */ + public function db_connect($persistent = FALSE) + { + $this->conn_id = ($persistent === TRUE) + ? pg_pconnect($this->dsn) + : pg_connect($this->dsn); + + if ($this->conn_id !== FALSE) + { + if ($persistent === TRUE + && pg_connection_status($this->conn_id) === PGSQL_CONNECTION_BAD + && pg_ping($this->conn_id) === FALSE + ) + { + return FALSE; + } + + empty($this->schema) OR $this->simple_query('SET search_path TO '.$this->schema.',public'); + } + + return $this->conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Reconnect + * + * Keep / reestablish the db connection if no queries have been + * sent for a length of time exceeding the server's idle timeout + * + * @return void + */ + public function reconnect() + { + if (pg_ping($this->conn_id) === FALSE) + { + $this->conn_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Set client character set + * + * @param string $charset + * @return bool + */ + protected function _db_set_charset($charset) + { + return (pg_set_client_encoding($this->conn_id, $charset) === 0); + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + if ( ! $this->conn_id OR ($pg_version = pg_version($this->conn_id)) === FALSE) + { + return FALSE; + } + + /* If PHP was compiled with PostgreSQL lib versions earlier + * than 7.4, pg_version() won't return the server version + * and so we'll have to fall back to running a query in + * order to get it. + */ + return isset($pg_version['server']) + ? $this->data_cache['version'] = $pg_version['server'] + : parent::version(); + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return resource + */ + protected function _execute($sql) + { + return pg_query($this->conn_id, $sql); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + return (bool) pg_query($this->conn_id, 'BEGIN'); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + return (bool) pg_query($this->conn_id, 'COMMIT'); + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + return (bool) pg_query($this->conn_id, 'ROLLBACK'); + } + + // -------------------------------------------------------------------- + + /** + * Determines if a query is a "write" type. + * + * @param string An SQL query string + * @return bool + */ + public function is_write_type($sql) + { + if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql)) + { + return FALSE; + } + + return parent::is_write_type($sql); + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependant string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + return pg_escape_string($this->conn_id, $str); + } + + // -------------------------------------------------------------------- + + /** + * "Smart" Escape String + * + * Escapes data based on type + * + * @param string $str + * @return mixed + */ + public function escape($str) + { + if (is_php('5.4.4') && (is_string($str) OR (is_object($str) && method_exists($str, '__toString')))) + { + return pg_escape_literal($this->conn_id, $str); + } + elseif (is_bool($str)) + { + return ($str) ? 'TRUE' : 'FALSE'; + } + + return parent::escape($str); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return pg_affected_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return string + */ + public function insert_id() + { + $v = pg_version($this->conn_id); + $v = isset($v['server']) ? $v['server'] : 0; // 'server' key is only available since PosgreSQL 7.4 + + $table = (func_num_args() > 0) ? func_get_arg(0) : NULL; + $column = (func_num_args() > 1) ? func_get_arg(1) : NULL; + + if ($table === NULL && $v >= '8.1') + { + $sql = 'SELECT LASTVAL() AS ins_id'; + } + elseif ($table !== NULL) + { + if ($column !== NULL && $v >= '8.0') + { + $sql = 'SELECT pg_get_serial_sequence(\''.$table."', '".$column."') AS seq"; + $query = $this->query($sql); + $query = $query->row(); + $seq = $query->seq; + } + else + { + // seq_name passed in table parameter + $seq = $table; + } + + $sql = 'SELECT CURRVAL(\''.$seq."') AS ins_id"; + } + else + { + return pg_last_oid($this->result_id); + } + + $query = $this->query($sql); + $query = $query->row(); + return (int) $query->ins_id; + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = \''.$this->schema."'"; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql.' AND "table_name" LIKE \'' + .$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * List column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT "column_name" + FROM "information_schema"."columns" + WHERE LOWER("table_name") = '.$this->escape(strtolower($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default" + FROM "information_schema"."columns" + WHERE LOWER("table_name") = '.$this->escape(strtolower($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->column_name; + $retval[$i]->type = $query[$i]->data_type; + $retval[$i]->max_length = ($query[$i]->character_maximum_length > 0) ? $query[$i]->character_maximum_length : $query[$i]->numeric_precision; + $retval[$i]->default = $query[$i]->column_default; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occured. + * + * @return array + */ + public function error() + { + return array('code' => '', 'message' => pg_last_error($this->conn_id)); + } + + // -------------------------------------------------------------------- + + /** + * ORDER BY + * + * @param string $orderby + * @param string $direction ASC, DESC or RANDOM + * @param bool $escape + * @return object + */ + public function order_by($orderby, $direction = '', $escape = NULL) + { + $direction = strtoupper(trim($direction)); + if ($direction === 'RANDOM') + { + if ( ! is_float($orderby) && ctype_digit((string) $orderby)) + { + $orderby = ($orderby > 1) + ? (float) '0.'.$orderby + : (float) $orderby; + } + + if (is_float($orderby)) + { + $this->simple_query('SET SEED '.$orderby); + } + + $orderby = $this->_random_keyword[0]; + $direction = ''; + $escape = FALSE; + } + + return parent::order_by($orderby, $direction, $escape); + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Update_Batch statement + * + * Generates a platform-specific batch update string from the supplied data + * + * @param string $table Table name + * @param array $values Update data + * @param string $index WHERE key + * @return string + */ + protected function _update_batch($table, $values, $index) + { + $ids = array(); + foreach ($values as $key => $val) + { + $ids[] = $val[$index]; + + foreach (array_keys($val) as $field) + { + if ($field !== $index) + { + $final[$field][] = 'WHEN '.$val[$index].' THEN '.$val[$field]; + } + } + } + + $cases = ''; + foreach ($final as $k => $v) + { + $cases .= $k.' = (CASE '.$index."\n" + .implode("\n", $v)."\n" + .'ELSE '.$k.' END), '; + } + + $this->where($index.' IN('.implode(',', $ids).')', NULL, FALSE); + + return 'UPDATE '.$table.' SET '.substr($cases, 0, -2).$this->_compile_wh('qb_where'); + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + $this->qb_limit = FALSE; + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + return $sql.' LIMIT '.$this->qb_limit.($this->qb_offset ? ' OFFSET '.$this->qb_offset : ''); + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + pg_close($this->conn_id); + } + +} diff --git a/system/database/drivers/postgre/postgre_forge.php b/system/database/drivers/postgre/postgre_forge.php new file mode 100644 index 0000000..8d985ba --- /dev/null +++ b/system/database/drivers/postgre/postgre_forge.php @@ -0,0 +1,205 @@ + 'INTEGER', + 'SMALLINT' => 'INTEGER', + 'INT' => 'BIGINT', + 'INT4' => 'BIGINT', + 'INTEGER' => 'BIGINT', + 'INT8' => 'NUMERIC', + 'BIGINT' => 'NUMERIC', + 'REAL' => 'DOUBLE PRECISION', + 'FLOAT' => 'DOUBLE PRECISION' + ); + + /** + * NULL value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $_null = 'NULL'; + + // -------------------------------------------------------------------- + + /** + * Class constructor + * + * @param object &$db Database object + * @return void + */ + public function __construct(&$db) + { + parent::__construct($db); + + if (version_compare($this->db->version(), '9.0', '>')) + { + $this->create_table_if = 'CREATE TABLE IF NOT EXISTS'; + } + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('DROP', 'ADD'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + return FALSE; + } + + if (version_compare($this->db->version(), '8', '>=') && isset($field[$i]['type'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TYPE '.$field[$i]['type'].$field[$i]['length']; + } + + if ( ! empty($field[$i]['default'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' SET DEFAULT '.$field[$i]['default']; + } + + if (isset($field[$i]['null'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .($field[$i]['null'] === TRUE ? ' DROP NOT NULL' : ' SET NOT NULL'); + } + + if ( ! empty($field[$i]['new_name'])) + { + $sqls[] = $sql.' RENAME COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TO '.$this->db->escape_identifiers($field[$i]['new_name']); + } + + if ( ! empty($field[$i]['comment'])) + { + $sqls[] = 'COMMENT ON COLUMN ' + .$this->db->escape_identifiers($table).'.'.$this->db->escape_identifiers($field[$i]['name']) + .' IS '.$field[$i]['comment']; + } + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + // Reset field lenghts for data types that don't support it + if (isset($attributes['CONSTRAINT']) && stripos($attributes['TYPE'], 'int') !== FALSE) + { + $attributes['CONSTRAINT'] = NULL; + } + + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE) + { + $field['type'] = ($field['type'] === 'NUMERIC') + ? 'BIGSERIAL' + : 'SERIAL'; + } + } + +} diff --git a/system/database/drivers/postgre/postgre_result.php b/system/database/drivers/postgre/postgre_result.php new file mode 100644 index 0000000..354bb08 --- /dev/null +++ b/system/database/drivers/postgre/postgre_result.php @@ -0,0 +1,182 @@ +num_rows) + ? $this->num_rows + : $this->num_rows = pg_num_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return pg_num_fields($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $field_names[] = pg_field_name($this->result_id, $i); + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = pg_field_name($this->result_id, $i); + $retval[$i]->type = pg_field_type($this->result_id, $i); + $retval[$i]->max_length = pg_field_size($this->result_id, $i); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_resource($this->result_id)) + { + pg_free_result($this->result_id); + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * @param int $n + * @return bool + */ + public function data_seek($n = 0) + { + return pg_result_seek($this->result_id, $n); + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return pg_fetch_assoc($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return pg_fetch_object($this->result_id, NULL, $class_name); + } + +} diff --git a/system/database/drivers/postgre/postgre_utility.php b/system/database/drivers/postgre/postgre_utility.php new file mode 100644 index 0000000..bb5e6e0 --- /dev/null +++ b/system/database/drivers/postgre/postgre_utility.php @@ -0,0 +1,78 @@ +db->display_error('db_unsupported_feature'); + } +} diff --git a/system/database/drivers/sqlite/index.html b/system/database/drivers/sqlite/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/sqlite/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/sqlite/sqlite_driver.php b/system/database/drivers/sqlite/sqlite_driver.php new file mode 100644 index 0000000..16b8c29 --- /dev/null +++ b/system/database/drivers/sqlite/sqlite_driver.php @@ -0,0 +1,330 @@ +database, 0666, $error) + : sqlite_open($this->database, 0666, $error); + + isset($error) && log_message('error', $error); + + return $conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + return isset($this->data_cache['version']) + ? $this->data_cache['version'] + : $this->data_cache['version'] = sqlite_libversion(); + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return resource + */ + protected function _execute($sql) + { + return $this->is_write_type($sql) + ? sqlite_exec($this->conn_id, $sql) + : sqlite_query($this->conn_id, $sql); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + return $this->simple_query('BEGIN TRANSACTION'); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + return $this->simple_query('COMMIT'); + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + return $this->simple_query('ROLLBACK'); + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependant string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + return sqlite_escape_string($str); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return sqlite_changes($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return int + */ + public function insert_id() + { + return sqlite_last_insert_rowid($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * List table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = "SELECT name FROM sqlite_master WHERE type='table'"; + + if ($prefix_limit !== FALSE && $this->dbprefix != '') + { + return $sql." AND 'name' LIKE '".$this->escape_like_str($this->dbprefix)."%' ".sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return bool + */ + protected function _list_columns($table = '') + { + // Not supported + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('PRAGMA TABLE_INFO('.$this->protect_identifiers($table, TRUE, NULL, FALSE).')')) === FALSE) + { + return FALSE; + } + + $query = $query->result_array(); + if (empty($query)) + { + return FALSE; + } + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]['name']; + $retval[$i]->type = $query[$i]['type']; + $retval[$i]->max_length = NULL; + $retval[$i]->default = $query[$i]['dflt_value']; + $retval[$i]->primary_key = isset($query[$i]['pk']) ? (int) $query[$i]['pk'] : 0; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occured. + * + * @return array + */ + public function error() + { + $error = array('code' => sqlite_last_error($this->conn_id)); + $error['message'] = sqlite_error_string($error['code']); + return $error; + } + + // -------------------------------------------------------------------- + + /** + * Replace statement + * + * Generates a platform-specific replace string from the supplied data + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string + */ + protected function _replace($table, $keys, $values) + { + return 'INSERT OR '.parent::_replace($table, $keys, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this function maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'DELETE FROM '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + sqlite_close($this->conn_id); + } + +} diff --git a/system/database/drivers/sqlite/sqlite_forge.php b/system/database/drivers/sqlite/sqlite_forge.php new file mode 100644 index 0000000..3ad3477 --- /dev/null +++ b/system/database/drivers/sqlite/sqlite_forge.php @@ -0,0 +1,205 @@ +db->database) OR ! @unlink($this->db->database)) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + elseif ( ! empty($this->db->data_cache['db_names'])) + { + $key = array_search(strtolower($this->db->database), array_map('strtolower', $this->db->data_cache['db_names']), TRUE); + if ($key !== FALSE) + { + unset($this->db->data_cache['db_names'][$key]); + } + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @todo implement drop_column(), modify_column() + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'DROP' OR $alter_type === 'CHANGE') + { + // drop_column(): + // BEGIN TRANSACTION; + // CREATE TEMPORARY TABLE t1_backup(a,b); + // INSERT INTO t1_backup SELECT a,b FROM t1; + // DROP TABLE t1; + // CREATE TABLE t1(a,b); + // INSERT INTO t1 SELECT a,b FROM t1_backup; + // DROP TABLE t1_backup; + // COMMIT; + + return FALSE; + } + + return parent::_alter_table($alter_type, $table, $field); + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'] + .$field['auto_increment'] + .$field['null'] + .$field['unique'] + .$field['default']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'ENUM': + case 'SET': + $attributes['TYPE'] = 'TEXT'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['type'] = 'INTEGER PRIMARY KEY'; + $field['default'] = ''; + $field['null'] = ''; + $field['unique'] = ''; + $field['auto_increment'] = ' AUTOINCREMENT'; + + $this->primary_keys = array(); + } + } + +} diff --git a/system/database/drivers/sqlite/sqlite_result.php b/system/database/drivers/sqlite/sqlite_result.php new file mode 100644 index 0000000..d40b98a --- /dev/null +++ b/system/database/drivers/sqlite/sqlite_result.php @@ -0,0 +1,164 @@ +num_rows) + ? $this->num_rows + : $this->num_rows = @sqlite_num_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return @sqlite_num_fields($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $field_names[$i] = sqlite_field_name($this->result_id, $i); + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = sqlite_field_name($this->result_id, $i); + $retval[$i]->type = NULL; + $retval[$i]->max_length = NULL; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * @param int $n + * @return bool + */ + public function data_seek($n = 0) + { + return sqlite_seek($this->result_id, $n); + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return sqlite_fetch_array($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return sqlite_fetch_object($this->result_id, $class_name); + } + +} diff --git a/system/database/drivers/sqlite/sqlite_utility.php b/system/database/drivers/sqlite/sqlite_utility.php new file mode 100644 index 0000000..59c46f9 --- /dev/null +++ b/system/database/drivers/sqlite/sqlite_utility.php @@ -0,0 +1,61 @@ +db->display_error('db_unsupported_feature'); + } + +} diff --git a/system/database/drivers/sqlite3/index.html b/system/database/drivers/sqlite3/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/sqlite3/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/sqlite3/sqlite3_driver.php b/system/database/drivers/sqlite3/sqlite3_driver.php new file mode 100644 index 0000000..9743499 --- /dev/null +++ b/system/database/drivers/sqlite3/sqlite3_driver.php @@ -0,0 +1,350 @@ +password) + ? new SQLite3($this->database) + : new SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password); + } + catch (Exception $e) + { + return FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + $version = SQLite3::version(); + return $this->data_cache['version'] = $version['versionString']; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @todo Implement use of SQLite3::querySingle(), if needed + * @param string $sql + * @return mixed SQLite3Result object or bool + */ + protected function _execute($sql) + { + return $this->is_write_type($sql) + ? $this->conn_id->exec($sql) + : $this->conn_id->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + return $this->conn_id->exec('BEGIN TRANSACTION'); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + return $this->conn_id->exec('END TRANSACTION'); + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + return $this->conn_id->exec('ROLLBACK'); + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependant string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + return $this->conn_id->escapeString($str); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return $this->conn_id->changes(); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return int + */ + public function insert_id() + { + return $this->conn_id->lastInsertRowID(); + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\'' + .(($prefix_limit !== FALSE && $this->dbprefix != '') + ? ' AND "NAME" LIKE \''.$this->escape_like_str($this->dbprefix).'%\' '.sprintf($this->_like_escape_str, $this->_like_escape_chr) + : ''); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * @param string $table Table name + * @return array + */ + public function list_fields($table) + { + // Is there a cached result? + if (isset($this->data_cache['field_names'][$table])) + { + return $this->data_cache['field_names'][$table]; + } + + if (($result = $this->query('PRAGMA TABLE_INFO('.$this->protect_identifiers($table, TRUE, NULL, FALSE).')')) === FALSE) + { + return FALSE; + } + + $this->data_cache['field_names'][$table] = array(); + foreach ($result->result_array() as $row) + { + $this->data_cache['field_names'][$table][] = $row['name']; + } + + return $this->data_cache['field_names'][$table]; + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('PRAGMA TABLE_INFO('.$this->protect_identifiers($table, TRUE, NULL, FALSE).')')) === FALSE) + { + return FALSE; + } + + $query = $query->result_array(); + if (empty($query)) + { + return FALSE; + } + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]['name']; + $retval[$i]->type = $query[$i]['type']; + $retval[$i]->max_length = NULL; + $retval[$i]->default = $query[$i]['dflt_value']; + $retval[$i]->primary_key = isset($query[$i]['pk']) ? (int) $query[$i]['pk'] : 0; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occured. + * + * @return array + */ + public function error() + { + return array('code' => $this->conn_id->lastErrorCode(), 'message' => $this->conn_id->lastErrorMsg()); + } + + // -------------------------------------------------------------------- + + /** + * Replace statement + * + * Generates a platform-specific replace string from the supplied data + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string + */ + protected function _replace($table, $keys, $values) + { + return 'INSERT OR '.parent::_replace($table, $keys, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'DELETE FROM '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + $this->conn_id->close(); + } + +} diff --git a/system/database/drivers/sqlite3/sqlite3_forge.php b/system/database/drivers/sqlite3/sqlite3_forge.php new file mode 100644 index 0000000..c45472f --- /dev/null +++ b/system/database/drivers/sqlite3/sqlite3_forge.php @@ -0,0 +1,225 @@ +db->version(), '3.3', '<')) + { + $this->_create_table_if = FALSE; + $this->_drop_table_if = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Create database + * + * @param string $db_name + * @return bool + */ + public function create_database($db_name) + { + // In SQLite, a database is created when you connect to the database. + // We'll return TRUE so that an error isn't generated + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Drop database + * + * @param string $db_name (ignored) + * @return bool + */ + public function drop_database($db_name) + { + // In SQLite, a database is dropped when we delete a file + if (file_exists($this->db->database)) + { + // We need to close the pseudo-connection first + $this->db->close(); + if ( ! @unlink($this->db->database)) + { + return $this->db->db_debug ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + elseif ( ! empty($this->db->data_cache['db_names'])) + { + $key = array_search(strtolower($this->db->database), array_map('strtolower', $this->db->data_cache['db_names']), TRUE); + if ($key !== FALSE) + { + unset($this->db->data_cache['db_names'][$key]); + } + } + + return TRUE; + } + + return $this->db->db_debug ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @todo implement drop_column(), modify_column() + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'DROP' OR $alter_type === 'CHANGE') + { + // drop_column(): + // BEGIN TRANSACTION; + // CREATE TEMPORARY TABLE t1_backup(a,b); + // INSERT INTO t1_backup SELECT a,b FROM t1; + // DROP TABLE t1; + // CREATE TABLE t1(a,b); + // INSERT INTO t1 SELECT a,b FROM t1_backup; + // DROP TABLE t1_backup; + // COMMIT; + + return FALSE; + } + + return parent::_alter_table($alter_type, $table, $field); + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'] + .$field['auto_increment'] + .$field['null'] + .$field['unique'] + .$field['default']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'ENUM': + case 'SET': + $attributes['TYPE'] = 'TEXT'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['type'] = 'INTEGER PRIMARY KEY'; + $field['default'] = ''; + $field['null'] = ''; + $field['unique'] = ''; + $field['auto_increment'] = ' AUTOINCREMENT'; + + $this->primary_keys = array(); + } + } + +} diff --git a/system/database/drivers/sqlite3/sqlite3_result.php b/system/database/drivers/sqlite3/sqlite3_result.php new file mode 100644 index 0000000..aa559ee --- /dev/null +++ b/system/database/drivers/sqlite3/sqlite3_result.php @@ -0,0 +1,194 @@ +result_id->numColumns(); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $field_names[] = $this->result_id->columnName($i); + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + static $data_types = array( + SQLITE3_INTEGER => 'integer', + SQLITE3_FLOAT => 'float', + SQLITE3_TEXT => 'text', + SQLITE3_BLOB => 'blob', + SQLITE3_NULL => 'null' + ); + + $retval = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $this->result_id->columnName($i); + + $type = $this->result_id->columnType($i); + $retval[$i]->type = isset($data_types[$type]) ? $data_types[$type] : $type; + + $retval[$i]->max_length = NULL; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_object($this->result_id)) + { + $this->result_id->finalize(); + $this->result_id = NULL; + } + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return $this->result_id->fetchArray(SQLITE3_ASSOC); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + // No native support for fetching rows as objects + if (($row = $this->result_id->fetchArray(SQLITE3_ASSOC)) === FALSE) + { + return FALSE; + } + elseif ($class_name === 'stdClass') + { + return (object) $row; + } + + $class_name = new $class_name(); + foreach (array_keys($row) as $key) + { + $class_name->$key = $row[$key]; + } + + return $class_name; + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * @param int $n (ignored) + * @return array + */ + public function data_seek($n = 0) + { + // Only resetting to the start of the result set is supported + return ($n > 0) ? FALSE : $this->result_id->reset(); + } + +} diff --git a/system/database/drivers/sqlite3/sqlite3_utility.php b/system/database/drivers/sqlite3/sqlite3_utility.php new file mode 100644 index 0000000..b47c086 --- /dev/null +++ b/system/database/drivers/sqlite3/sqlite3_utility.php @@ -0,0 +1,61 @@ +db->display_error('db_unsupported_feature'); + } + +} diff --git a/system/database/drivers/sqlsrv/index.html b/system/database/drivers/sqlsrv/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/sqlsrv/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/sqlsrv/sqlsrv_driver.php b/system/database/drivers/sqlsrv/sqlsrv_driver.php new file mode 100644 index 0000000..c55d5f7 --- /dev/null +++ b/system/database/drivers/sqlsrv/sqlsrv_driver.php @@ -0,0 +1,543 @@ +scrollable === NULL) + { + $this->scrollable = defined('SQLSRV_CURSOR_CLIENT_BUFFERED') + ? SQLSRV_CURSOR_CLIENT_BUFFERED + : FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Database connection + * + * @param bool $pooling + * @return resource + */ + public function db_connect($pooling = FALSE) + { + $charset = in_array(strtolower($this->char_set), array('utf-8', 'utf8'), TRUE) + ? 'UTF-8' : SQLSRV_ENC_CHAR; + + $connection = array( + 'UID' => empty($this->username) ? '' : $this->username, + 'PWD' => empty($this->password) ? '' : $this->password, + 'Database' => $this->database, + 'ConnectionPooling' => ($pooling === TRUE) ? 1 : 0, + 'CharacterSet' => $charset, + 'Encrypt' => ($this->encrypt === TRUE) ? 1 : 0, + 'ReturnDatesAsStrings' => 1 + ); + + // If the username and password are both empty, assume this is a + // 'Windows Authentication Mode' connection. + if (empty($connection['UID']) && empty($connection['PWD'])) + { + unset($connection['UID'], $connection['PWD']); + } + + if (FALSE !== ($this->conn_id = sqlsrv_connect($this->hostname, $connection))) + { + // Determine how identifiers are escaped + $query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi'); + $query = $query->row_array(); + $this->_quoted_identifier = empty($query) ? FALSE : (bool) $query['qi']; + $this->_escape_char = ($this->_quoted_identifier) ? '"' : array('[', ']'); + } + + return $this->conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Select the database + * + * @param string $database + * @return bool + */ + public function db_select($database = '') + { + if ($database === '') + { + $database = $this->database; + } + + if ($this->_execute('USE '.$this->escape_identifiers($database))) + { + $this->database = $database; + $this->data_cache = array(); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return resource + */ + protected function _execute($sql) + { + return ($this->scrollable === FALSE OR $this->is_write_type($sql)) + ? sqlsrv_query($this->conn_id, $sql) + : sqlsrv_query($this->conn_id, $sql, NULL, array('Scrollable' => $this->scrollable)); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + return sqlsrv_begin_transaction($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + return sqlsrv_commit($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + return sqlsrv_rollback($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return sqlsrv_rows_affected($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * Returns the last id created in the Identity column. + * + * @return string + */ + public function insert_id() + { + return $this->query('SELECT SCOPE_IDENTITY() AS insert_id')->row()->insert_id; + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + if ( ! $this->conn_id OR ($info = sqlsrv_server_info($this->conn_id)) === FALSE) + { + return FALSE; + } + + return $this->data_cache['version'] = $info['SQLServerVersion']; + } + + // -------------------------------------------------------------------- + + /** + * List table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool + * @return string $prefix_limit + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT '.$this->escape_identifiers('name') + .' FROM '.$this->escape_identifiers('sysobjects') + .' WHERE '.$this->escape_identifiers('type')." = 'U'"; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + $sql .= ' AND '.$this->escape_identifiers('name')." LIKE '".$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_escape_like_str, $this->_escape_like_chr); + } + + return $sql.' ORDER BY '.$this->escape_identifiers('name'); + } + + // -------------------------------------------------------------------- + + /** + * List column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->COLUMN_NAME; + $retval[$i]->type = $query[$i]->DATA_TYPE; + $retval[$i]->max_length = ($query[$i]->CHARACTER_MAXIMUM_LENGTH > 0) ? $query[$i]->CHARACTER_MAXIMUM_LENGTH : $query[$i]->NUMERIC_PRECISION; + $retval[$i]->default = $query[$i]->COLUMN_DEFAULT; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occured. + * + * @return array + */ + public function error() + { + $error = array('code' => '00000', 'message' => ''); + $sqlsrv_errors = sqlsrv_errors(SQLSRV_ERR_ERRORS); + + if ( ! is_array($sqlsrv_errors)) + { + return $error; + } + + $sqlsrv_error = array_shift($sqlsrv_errors); + if (isset($sqlsrv_error['SQLSTATE'])) + { + $error['code'] = isset($sqlsrv_error['code']) ? $sqlsrv_error['SQLSTATE'].'/'.$sqlsrv_error['code'] : $sqlsrv_error['SQLSTATE']; + } + elseif (isset($sqlsrv_error['code'])) + { + $error['code'] = $sqlsrv_error['code']; + } + + if (isset($sqlsrv_error['message'])) + { + $error['message'] = $sqlsrv_error['message']; + } + + return $error; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE TABLE '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + if ($this->qb_limit) + { + return 'WITH ci_delete AS (SELECT TOP '.$this->qb_limit.' * FROM '.$table.$this->_compile_wh('qb_where').') DELETE FROM ci_delete'; + } + + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + // As of SQL Server 2012 (11.0.*) OFFSET is supported + if (version_compare($this->version(), '11', '>=')) + { + // SQL Server OFFSET-FETCH can be used only with the ORDER BY clause + empty($this->qb_orderby) && $sql .= ' ORDER BY 1'; + + return $sql.' OFFSET '.(int) $this->qb_offset.' ROWS FETCH NEXT '.$this->qb_limit.' ROWS ONLY'; + } + + $limit = $this->qb_offset + $this->qb_limit; + + // An ORDER BY clause is required for ROW_NUMBER() to work + if ($this->qb_offset && ! empty($this->qb_orderby)) + { + $orderby = $this->_compile_order_by(); + + // We have to strip the ORDER BY clause + $sql = trim(substr($sql, 0, strrpos($sql, $orderby))); + + // Get the fields to select from our subquery, so that we can avoid CI_rownum appearing in the actual results + if (count($this->qb_select) === 0) + { + $select = '*'; // Inevitable + } + else + { + // Use only field names and their aliases, everything else is out of our scope. + $select = array(); + $field_regexp = ($this->_quoted_identifier) + ? '("[^\"]+")' : '(\[[^\]]+\])'; + for ($i = 0, $c = count($this->qb_select); $i < $c; $i++) + { + $select[] = preg_match('/(?:\s|\.)'.$field_regexp.'$/i', $this->qb_select[$i], $m) + ? $m[1] : $this->qb_select[$i]; + } + $select = implode(', ', $select); + } + + return 'SELECT '.$select." FROM (\n\n" + .preg_replace('/^(SELECT( DISTINCT)?)/i', '\\1 ROW_NUMBER() OVER('.trim($orderby).') AS '.$this->escape_identifiers('CI_rownum').', ', $sql) + ."\n\n) ".$this->escape_identifiers('CI_subquery') + ."\nWHERE ".$this->escape_identifiers('CI_rownum').' BETWEEN '.($this->qb_offset + 1).' AND '.$limit; + } + + return preg_replace('/(^\SELECT (DISTINCT)?)/i','\\1 TOP '.$limit.' ', $sql); + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string|bool + */ + protected function _insert_batch($table, $keys, $values) + { + // Multiple-value inserts are only supported as of SQL Server 2008 + if (version_compare($this->version(), '10', '>=')) + { + return parent::_insert_batch($table, $keys, $values); + } + + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + sqlsrv_close($this->conn_id); + } + +} diff --git a/system/database/drivers/sqlsrv/sqlsrv_forge.php b/system/database/drivers/sqlsrv/sqlsrv_forge.php new file mode 100644 index 0000000..4f0ce9d --- /dev/null +++ b/system/database/drivers/sqlsrv/sqlsrv_forge.php @@ -0,0 +1,149 @@ + 'SMALLINT', + 'SMALLINT' => 'INT', + 'INT' => 'BIGINT', + 'REAL' => 'FLOAT' + ); + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('ADD', 'DROP'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' ALTER COLUMN '; + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + $sqls[] = $sql.$this->_process_column($field[$i]); + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + if (isset($attributes['CONSTRAINT']) && strpos($attributes['TYPE'], 'INT') !== FALSE) + { + unset($attributes['CONSTRAINT']); + } + + switch (strtoupper($attributes['TYPE'])) + { + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'INTEGER': + $attributes['TYPE'] = 'INT'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['auto_increment'] = ' IDENTITY(1,1)'; + } + } + +} diff --git a/system/database/drivers/sqlsrv/sqlsrv_result.php b/system/database/drivers/sqlsrv/sqlsrv_result.php new file mode 100644 index 0000000..fde7264 --- /dev/null +++ b/system/database/drivers/sqlsrv/sqlsrv_result.php @@ -0,0 +1,193 @@ +scrollable = $driver_object->scrollable; + } + + // -------------------------------------------------------------------- + + /** + * Number of rows in the result set + * + * @return int + */ + public function num_rows() + { + // sqlsrv_num_rows() doesn't work with the FORWARD and DYNAMIC cursors (FALSE is the same as FORWARD) + if ( ! in_array($this->scrollable, array(FALSE, SQLSRV_CURSOR_FORWARD, SQLSRV_CURSOR_DYNAMIC), TRUE)) + { + return parent::num_rows(); + } + + return is_int($this->num_rows) + ? $this->num_rows + : $this->num_rows = sqlsrv_num_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return @sqlsrv_num_fields($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + foreach (sqlsrv_field_metadata($this->result_id) as $offset => $field) + { + $field_names[] = $field['Name']; + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + foreach (sqlsrv_field_metadata($this->result_id) as $i => $field) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $field['Name']; + $retval[$i]->type = $field['Type']; + $retval[$i]->max_length = $field['Size']; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_resource($this->result_id)) + { + sqlsrv_free_stmt($this->result_id); + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return sqlsrv_fetch_array($this->result_id, SQLSRV_FETCH_ASSOC); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return sqlsrv_fetch_object($this->result_id, $class_name); + } + +} diff --git a/system/database/drivers/sqlsrv/sqlsrv_utility.php b/system/database/drivers/sqlsrv/sqlsrv_utility.php new file mode 100644 index 0000000..726fe3e --- /dev/null +++ b/system/database/drivers/sqlsrv/sqlsrv_utility.php @@ -0,0 +1,77 @@ +db->display_error('db_unsupported_feature'); + } + +} diff --git a/system/database/index.html b/system/database/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/fonts/index.html b/system/fonts/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/fonts/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/fonts/texb.ttf b/system/fonts/texb.ttf new file mode 100644 index 0000000000000000000000000000000000000000..383c88b86b7c17e2e284732af48b2bfc359647ae GIT binary patch literal 143830 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 z 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 n`i '', + 'img_path' => '', + 'img_url' => '', + 'img_width' => '150', + 'img_height' => '30', + 'font_path' => '', + 'expiration' => 7200, + 'word_length' => 8, + 'font_size' => 16, + 'img_id' => '', + 'pool' => '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + 'colors' => array( + 'background' => array(255,255,255), + 'border' => array(153,102,102), + 'text' => array(204,153,153), + 'grid' => array(255,182,182) + ) + ); + + foreach ($defaults as $key => $val) + { + if ( ! is_array($data) && empty($$key)) + { + $$key = $val; + } + else + { + $$key = isset($data[$key]) ? $data[$key] : $val; + } + } + + if ($img_path === '' OR $img_url === '' + OR ! is_dir($img_path) OR ! is_really_writable($img_path) + OR ! extension_loaded('gd')) + { + return FALSE; + } + + // ----------------------------------- + // Remove old images + // ----------------------------------- + + $now = microtime(TRUE); + + $current_dir = @opendir($img_path); + while ($filename = @readdir($current_dir)) + { + if (in_array(substr($filename, -4), array('.jpg', '.png')) + && (str_replace(array('.jpg', '.png'), '', $filename) + $expiration) < $now) + { + @unlink($img_path.$filename); + } + } + + @closedir($current_dir); + + // ----------------------------------- + // Do we have a "word" yet? + // ----------------------------------- + + if (empty($word)) + { + $word = ''; + $pool_length = strlen($pool); + $rand_max = $pool_length - 1; + + // PHP7 or a suitable polyfill + if (function_exists('random_int')) + { + try + { + for ($i = 0; $i < $word_length; $i++) + { + $word .= $pool[random_int(0, $rand_max)]; + } + } + catch (Exception $e) + { + // This means fallback to the next possible + // alternative to random_int() + $word = ''; + } + } + } + + if (empty($word)) + { + // Nobody will have a larger character pool than + // 256 characters, but let's handle it just in case ... + // + // No, I do not care that the fallback to mt_rand() can + // handle it; if you trigger this, you're very obviously + // trying to break it. -- Narf + if ($pool_length > 256) + { + return FALSE; + } + + // We'll try using the operating system's PRNG first, + // which we can access through CI_Security::get_random_bytes() + $security = get_instance()->security; + + // To avoid numerous get_random_bytes() calls, we'll + // just try fetching as much bytes as we need at once. + if (($bytes = $security->get_random_bytes($pool_length)) !== FALSE) + { + $byte_index = $word_index = 0; + while ($word_index < $word_length) + { + // Do we have more random data to use? + // It could be exhausted by previous iterations + // ignoring bytes higher than $rand_max. + if ($byte_index === $pool_length) + { + // No failures should be possible if the + // first get_random_bytes() call didn't + // return FALSE, but still ... + for ($i = 0; $i < 5; $i++) + { + if (($bytes = $security->get_random_bytes($pool_length)) === FALSE) + { + continue; + } + + $byte_index = 0; + break; + } + + if ($bytes === FALSE) + { + // Sadly, this means fallback to mt_rand() + $word = ''; + break; + } + } + + list(, $rand_index) = unpack('C', $bytes[$byte_index++]); + if ($rand_index > $rand_max) + { + continue; + } + + $word .= $pool[$rand_index]; + $word_index++; + } + } + } + + if (empty($word)) + { + for ($i = 0; $i < $word_length; $i++) + { + $word .= $pool[mt_rand(0, $rand_max)]; + } + } + elseif ( ! is_string($word)) + { + $word = (string) $word; + } + + // ----------------------------------- + // Determine angle and position + // ----------------------------------- + $length = strlen($word); + $angle = ($length >= 6) ? mt_rand(-($length-6), ($length-6)) : 0; + $x_axis = mt_rand(6, (360/$length)-16); + $y_axis = ($angle >= 0) ? mt_rand($img_height, $img_width) : mt_rand(6, $img_height); + + // Create image + // PHP.net recommends imagecreatetruecolor(), but it isn't always available + $im = function_exists('imagecreatetruecolor') + ? imagecreatetruecolor($img_width, $img_height) + : imagecreate($img_width, $img_height); + + // ----------------------------------- + // Assign colors + // ---------------------------------- + + is_array($colors) OR $colors = $defaults['colors']; + + foreach (array_keys($defaults['colors']) as $key) + { + // Check for a possible missing value + is_array($colors[$key]) OR $colors[$key] = $defaults['colors'][$key]; + $colors[$key] = imagecolorallocate($im, $colors[$key][0], $colors[$key][1], $colors[$key][2]); + } + + // Create the rectangle + ImageFilledRectangle($im, 0, 0, $img_width, $img_height, $colors['background']); + + // ----------------------------------- + // Create the spiral pattern + // ----------------------------------- + $theta = 1; + $thetac = 7; + $radius = 16; + $circles = 20; + $points = 32; + + for ($i = 0, $cp = ($circles * $points) - 1; $i < $cp; $i++) + { + $theta += $thetac; + $rad = $radius * ($i / $points); + $x = ($rad * cos($theta)) + $x_axis; + $y = ($rad * sin($theta)) + $y_axis; + $theta += $thetac; + $rad1 = $radius * (($i + 1) / $points); + $x1 = ($rad1 * cos($theta)) + $x_axis; + $y1 = ($rad1 * sin($theta)) + $y_axis; + imageline($im, $x, $y, $x1, $y1, $colors['grid']); + $theta -= $thetac; + } + + // ----------------------------------- + // Write the text + // ----------------------------------- + + $use_font = ($font_path !== '' && file_exists($font_path) && function_exists('imagettftext')); + if ($use_font === FALSE) + { + ($font_size > 5) && $font_size = 5; + $x = mt_rand(0, $img_width / ($length / 3)); + $y = 0; + } + else + { + ($font_size > 30) && $font_size = 30; + $x = mt_rand(0, $img_width / ($length / 1.5)); + $y = $font_size + 2; + } + + for ($i = 0; $i < $length; $i++) + { + if ($use_font === FALSE) + { + $y = mt_rand(0 , $img_height / 2); + imagestring($im, $font_size, $x, $y, $word[$i], $colors['text']); + $x += ($font_size * 2); + } + else + { + $y = mt_rand($img_height / 2, $img_height - 3); + imagettftext($im, $font_size, $angle, $x, $y, $colors['text'], $font_path, $word[$i]); + $x += $font_size; + } + } + + // Create the border + imagerectangle($im, 0, 0, $img_width - 1, $img_height - 1, $colors['border']); + + // ----------------------------------- + // Generate the image + // ----------------------------------- + $img_url = rtrim($img_url, '/').'/'; + + if (function_exists('imagejpeg')) + { + $img_filename = $now.'.jpg'; + imagejpeg($im, $img_path.$img_filename); + } + elseif (function_exists('imagepng')) + { + $img_filename = $now.'.png'; + imagepng($im, $img_path.$img_filename); + } + else + { + return FALSE; + } + + $img = ' '; + ImageDestroy($im); + + return array('word' => $word, 'time' => $now, 'image' => $img, 'filename' => $img_filename); + } +} diff --git a/system/helpers/cookie_helper.php b/system/helpers/cookie_helper.php new file mode 100644 index 0000000..ca43244 --- /dev/null +++ b/system/helpers/cookie_helper.php @@ -0,0 +1,113 @@ +input->set_cookie($name, $value, $expire, $domain, $path, $prefix, $secure, $httponly); + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('get_cookie')) +{ + /** + * Fetch an item from the COOKIE array + * + * @param string + * @param bool + * @return mixed + */ + function get_cookie($index, $xss_clean = NULL) + { + is_bool($xss_clean) OR $xss_clean = (config_item('global_xss_filtering') === TRUE); + $prefix = isset($_COOKIE[$index]) ? '' : config_item('cookie_prefix'); + return get_instance()->input->cookie($prefix.$index, $xss_clean); + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('delete_cookie')) +{ + /** + * Delete a COOKIE + * + * @param mixed + * @param string the cookie domain. Usually: .yourdomain.com + * @param string the cookie path + * @param string the cookie prefix + * @return void + */ + function delete_cookie($name, $domain = '', $path = '/', $prefix = '') + { + set_cookie($name, '', '', $domain, $path, $prefix); + } +} diff --git a/system/helpers/date_helper.php b/system/helpers/date_helper.php new file mode 100644 index 0000000..5f1fcf0 --- /dev/null +++ b/system/helpers/date_helper.php @@ -0,0 +1,741 @@ +format('j-n-Y G:i:s'), '%d-%d-%d %d:%d:%d', $day, $month, $year, $hour, $minute, $second); + + return mktime($hour, $minute, $second, $month, $day, $year); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('mdate')) +{ + /** + * Convert MySQL Style Datecodes + * + * This function is identical to PHPs date() function, + * except that it allows date codes to be formatted using + * the MySQL style, where each code letter is preceded + * with a percent sign: %Y %m %d etc... + * + * The benefit of doing dates this way is that you don't + * have to worry about escaping your text letters that + * match the date codes. + * + * @param string + * @param int + * @return int + */ + function mdate($datestr = '', $time = '') + { + if ($datestr === '') + { + return ''; + } + elseif (empty($time)) + { + $time = now(); + } + + $datestr = str_replace( + '%\\', + '', + preg_replace('/([a-z]+?){1}/i', '\\\\\\1', $datestr) + ); + + return date($datestr, $time); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('standard_date')) +{ + /** + * Standard Date + * + * Returns a date formatted according to the submitted standard. + * + * As of PHP 5.2, the DateTime extension provides constants that + * serve for the exact same purpose and are used with date(). + * + * @todo Remove in version 3.1+. + * @deprecated 3.0.0 Use PHP's native date() instead. + * @link http://www.php.net/manual/en/class.datetime.php#datetime.constants.types + * + * @example date(DATE_RFC822, now()); // default + * @example date(DATE_W3C, $time); // a different format and time + * + * @param string $fmt = 'DATE_RFC822' the chosen format + * @param int $time = NULL Unix timestamp + * @return string + */ + function standard_date($fmt = 'DATE_RFC822', $time = NULL) + { + if (empty($time)) + { + $time = now(); + } + + // Procedural style pre-defined constants from the DateTime extension + if (strpos($fmt, 'DATE_') !== 0 OR defined($fmt) === FALSE) + { + return FALSE; + } + + return date(constant($fmt), $time); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('timespan')) +{ + /** + * Timespan + * + * Returns a span of seconds in this format: + * 10 days 14 hours 36 minutes 47 seconds + * + * @param int a number of seconds + * @param int Unix timestamp + * @param int a number of display units + * @return string + */ + function timespan($seconds = 1, $time = '', $units = 7) + { + $CI =& get_instance(); + $CI->lang->load('date'); + + is_numeric($seconds) OR $seconds = 1; + is_numeric($time) OR $time = time(); + is_numeric($units) OR $units = 7; + + $seconds = ($time <= $seconds) ? 1 : $time - $seconds; + + $str = array(); + $years = floor($seconds / 31557600); + + if ($years > 0) + { + $str[] = $years.' '.$CI->lang->line($years > 1 ? 'date_years' : 'date_year'); + } + + $seconds -= $years * 31557600; + $months = floor($seconds / 2629743); + + if (count($str) < $units && ($years > 0 OR $months > 0)) + { + if ($months > 0) + { + $str[] = $months.' '.$CI->lang->line($months > 1 ? 'date_months' : 'date_month'); + } + + $seconds -= $months * 2629743; + } + + $weeks = floor($seconds / 604800); + + if (count($str) < $units && ($years > 0 OR $months > 0 OR $weeks > 0)) + { + if ($weeks > 0) + { + $str[] = $weeks.' '.$CI->lang->line($weeks > 1 ? 'date_weeks' : 'date_week'); + } + + $seconds -= $weeks * 604800; + } + + $days = floor($seconds / 86400); + + if (count($str) < $units && ($months > 0 OR $weeks > 0 OR $days > 0)) + { + if ($days > 0) + { + $str[] = $days.' '.$CI->lang->line($days > 1 ? 'date_days' : 'date_day'); + } + + $seconds -= $days * 86400; + } + + $hours = floor($seconds / 3600); + + if (count($str) < $units && ($days > 0 OR $hours > 0)) + { + if ($hours > 0) + { + $str[] = $hours.' '.$CI->lang->line($hours > 1 ? 'date_hours' : 'date_hour'); + } + + $seconds -= $hours * 3600; + } + + $minutes = floor($seconds / 60); + + if (count($str) < $units && ($days > 0 OR $hours > 0 OR $minutes > 0)) + { + if ($minutes > 0) + { + $str[] = $minutes.' '.$CI->lang->line($minutes > 1 ? 'date_minutes' : 'date_minute'); + } + + $seconds -= $minutes * 60; + } + + if (count($str) === 0) + { + $str[] = $seconds.' '.$CI->lang->line($seconds > 1 ? 'date_seconds' : 'date_second'); + } + + return implode(', ', $str); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('days_in_month')) +{ + /** + * Number of days in a month + * + * Takes a month/year as input and returns the number of days + * for the given month/year. Takes leap years into consideration. + * + * @param int a numeric month + * @param int a numeric year + * @return int + */ + function days_in_month($month = 0, $year = '') + { + if ($month < 1 OR $month > 12) + { + return 0; + } + elseif ( ! is_numeric($year) OR strlen($year) !== 4) + { + $year = date('Y'); + } + + if (defined('CAL_GREGORIAN')) + { + return cal_days_in_month(CAL_GREGORIAN, $month, $year); + } + + if ($year >= 1970) + { + return (int) date('t', mktime(12, 0, 0, $month, 1, $year)); + } + + if ($month == 2) + { + if ($year % 400 === 0 OR ($year % 4 === 0 && $year % 100 !== 0)) + { + return 29; + } + } + + $days_in_month = array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); + return $days_in_month[$month - 1]; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('local_to_gmt')) +{ + /** + * Converts a local Unix timestamp to GMT + * + * @param int Unix timestamp + * @return int + */ + function local_to_gmt($time = '') + { + if ($time === '') + { + $time = time(); + } + + return mktime( + gmdate('G', $time), + gmdate('i', $time), + gmdate('s', $time), + gmdate('n', $time), + gmdate('j', $time), + gmdate('Y', $time) + ); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('gmt_to_local')) +{ + /** + * Converts GMT time to a localized value + * + * Takes a Unix timestamp (in GMT) as input, and returns + * at the local value based on the timezone and DST setting + * submitted + * + * @param int Unix timestamp + * @param string timezone + * @param bool whether DST is active + * @return int + */ + function gmt_to_local($time = '', $timezone = 'UTC', $dst = FALSE) + { + if ($time === '') + { + return now(); + } + + $time += timezones($timezone) * 3600; + + return ($dst === TRUE) ? $time + 3600 : $time; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('mysql_to_unix')) +{ + /** + * Converts a MySQL Timestamp to Unix + * + * @param int MySQL timestamp YYYY-MM-DD HH:MM:SS + * @return int Unix timstamp + */ + function mysql_to_unix($time = '') + { + // We'll remove certain characters for backward compatibility + // since the formatting changed with MySQL 4.1 + // YYYY-MM-DD HH:MM:SS + + $time = str_replace(array('-', ':', ' '), '', $time); + + // YYYYMMDDHHMMSS + return mktime( + substr($time, 8, 2), + substr($time, 10, 2), + substr($time, 12, 2), + substr($time, 4, 2), + substr($time, 6, 2), + substr($time, 0, 4) + ); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('unix_to_human')) +{ + /** + * Unix to "Human" + * + * Formats Unix timestamp to the following prototype: 2006-08-21 11:35 PM + * + * @param int Unix timestamp + * @param bool whether to show seconds + * @param string format: us or euro + * @return string + */ + function unix_to_human($time = '', $seconds = FALSE, $fmt = 'us') + { + $r = date('Y', $time).'-'.date('m', $time).'-'.date('d', $time).' '; + + if ($fmt === 'us') + { + $r .= date('h', $time).':'.date('i', $time); + } + else + { + $r .= date('H', $time).':'.date('i', $time); + } + + if ($seconds) + { + $r .= ':'.date('s', $time); + } + + if ($fmt === 'us') + { + return $r.' '.date('A', $time); + } + + return $r; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('human_to_unix')) +{ + /** + * Convert "human" date to GMT + * + * Reverses the above process + * + * @param string format: us or euro + * @return int + */ + function human_to_unix($datestr = '') + { + if ($datestr === '') + { + return FALSE; + } + + $datestr = preg_replace('/\040+/', ' ', trim($datestr)); + + if ( ! preg_match('/^(\d{2}|\d{4})\-[0-9]{1,2}\-[0-9]{1,2}\s[0-9]{1,2}:[0-9]{1,2}(?::[0-9]{1,2})?(?:\s[AP]M)?$/i', $datestr)) + { + return FALSE; + } + + sscanf($datestr, '%d-%d-%d %s %s', $year, $month, $day, $time, $ampm); + sscanf($time, '%d:%d:%d', $hour, $min, $sec); + isset($sec) OR $sec = 0; + + if (isset($ampm)) + { + $ampm = strtolower($ampm); + + if ($ampm[0] === 'p' && $hour < 12) + { + $hour += 12; + } + elseif ($ampm[0] === 'a' && $hour === 12) + { + $hour = 0; + } + } + + return mktime($hour, $min, $sec, $month, $day, $year); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('nice_date')) +{ + /** + * Turns many "reasonably-date-like" strings into something + * that is actually useful. This only works for dates after unix epoch. + * + * @param string The terribly formatted date-like string + * @param string Date format to return (same as php date function) + * @return string + */ + function nice_date($bad_date = '', $format = FALSE) + { + if (empty($bad_date)) + { + return 'Unknown'; + } + elseif (empty($format)) + { + $format = 'U'; + } + + // Date like: YYYYMM + if (preg_match('/^\d{6}$/i', $bad_date)) + { + if (in_array(substr($bad_date, 0, 2), array('19', '20'))) + { + $year = substr($bad_date, 0, 4); + $month = substr($bad_date, 4, 2); + } + else + { + $month = substr($bad_date, 0, 2); + $year = substr($bad_date, 2, 4); + } + + return date($format, strtotime($year.'-'.$month.'-01')); + } + + // Date Like: YYYYMMDD + if (preg_match('/^(\d{2})\d{2}(\d{4})$/i', $bad_date, $matches)) + { + return date($format, strtotime($matches[1].'/01/'.$matches[2])); + } + + // Date Like: MM-DD-YYYY __or__ M-D-YYYY (or anything in between) + if (preg_match('/^(\d{1,2})-(\d{1,2})-(\d{4})$/i', $bad_date, $matches)) + { + return date($format, strtotime($matches[3].'-'.$matches[1].'-'.$matches[2])); + } + + // Any other kind of string, when converted into UNIX time, + // produces "0 seconds after epoc..." is probably bad... + // return "Invalid Date". + if (date('U', strtotime($bad_date)) === '0') + { + return 'Invalid Date'; + } + + // It's probably a valid-ish date format already + return date($format, strtotime($bad_date)); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('timezone_menu')) +{ + /** + * Timezone Menu + * + * Generates a drop-down menu of timezones. + * + * @param string timezone + * @param string classname + * @param string menu name + * @param mixed attributes + * @return string + */ + function timezone_menu($default = 'UTC', $class = '', $name = 'timezones', $attributes = '') + { + $CI =& get_instance(); + $CI->lang->load('date'); + + $default = ($default === 'GMT') ? 'UTC' : $default; + + $menu = ''; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('timezones')) +{ + /** + * Timezones + * + * Returns an array of timezones. This is a helper function + * for various other ones in this library + * + * @param string timezone + * @return string + */ + function timezones($tz = '') + { + // Note: Don't change the order of these even though + // some items appear to be in the wrong order + + $zones = array( + 'UM12' => -12, + 'UM11' => -11, + 'UM10' => -10, + 'UM95' => -9.5, + 'UM9' => -9, + 'UM8' => -8, + 'UM7' => -7, + 'UM6' => -6, + 'UM5' => -5, + 'UM45' => -4.5, + 'UM4' => -4, + 'UM35' => -3.5, + 'UM3' => -3, + 'UM2' => -2, + 'UM1' => -1, + 'UTC' => 0, + 'UP1' => +1, + 'UP2' => +2, + 'UP3' => +3, + 'UP35' => +3.5, + 'UP4' => +4, + 'UP45' => +4.5, + 'UP5' => +5, + 'UP55' => +5.5, + 'UP575' => +5.75, + 'UP6' => +6, + 'UP65' => +6.5, + 'UP7' => +7, + 'UP8' => +8, + 'UP875' => +8.75, + 'UP9' => +9, + 'UP95' => +9.5, + 'UP10' => +10, + 'UP105' => +10.5, + 'UP11' => +11, + 'UP115' => +11.5, + 'UP12' => +12, + 'UP1275' => +12.75, + 'UP13' => +13, + 'UP14' => +14 + ); + + if ($tz === '') + { + return $zones; + } + + return isset($zones[$tz]) ? $zones[$tz] : 0; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('date_range')) +{ + /** + * Date range + * + * Returns a list of dates within a specified period. + * + * @param int unix_start UNIX timestamp of period start date + * @param int unix_end|days UNIX timestamp of period end date + * or interval in days. + * @param mixed is_unix Specifies whether the second parameter + * is a UNIX timestamp or a day interval + * - TRUE or 'unix' for a timestamp + * - FALSE or 'days' for an interval + * @param string date_format Output date format, same as in date() + * @return array + */ + function date_range($unix_start = '', $mixed = '', $is_unix = TRUE, $format = 'Y-m-d') + { + if ($unix_start == '' OR $mixed == '' OR $format == '') + { + return FALSE; + } + + $is_unix = ! ( ! $is_unix OR $is_unix === 'days'); + + // Validate input and try strtotime() on invalid timestamps/intervals, just in case + if ( ( ! ctype_digit((string) $unix_start) && ($unix_start = @strtotime($unix_start)) === FALSE) + OR ( ! ctype_digit((string) $mixed) && ($is_unix === FALSE OR ($mixed = @strtotime($mixed)) === FALSE)) + OR ($is_unix === TRUE && $mixed < $unix_start)) + { + return FALSE; + } + + if ($is_unix && ($unix_start == $mixed OR date($format, $unix_start) === date($format, $mixed))) + { + return array(date($format, $unix_start)); + } + + $range = array(); + + $from = new DateTime(); + $from->setTimestamp($unix_start); + + if ($is_unix) + { + $arg = new DateTime(); + $arg->setTimestamp($mixed); + } + else + { + $arg = (int) $mixed; + } + + $period = new DatePeriod($from, new DateInterval('P1D'), $arg); + foreach ($period as $date) + { + $range[] = $date->format($format); + } + + /* If a period end date was passed to the DatePeriod constructor, it might not + * be in our results. Not sure if this is a bug or it's just possible because + * the end date might actually be less than 24 hours away from the previously + * generated DateTime object, but either way - we have to append it manually. + */ + if ( ! is_int($arg) && $range[count($range) - 1] !== $arg->format($format)) + { + $range[] = $arg->format($format); + } + + return $range; + } +} diff --git a/system/helpers/directory_helper.php b/system/helpers/directory_helper.php new file mode 100644 index 0000000..cdc4c16 --- /dev/null +++ b/system/helpers/directory_helper.php @@ -0,0 +1,101 @@ + 0) && is_dir($source_dir.$file)) + { + $filedata[$file] = directory_map($source_dir.$file, $new_depth, $hidden); + } + else + { + $filedata[] = $file; + } + } + + closedir($fp); + return $filedata; + } + + return FALSE; + } +} diff --git a/system/helpers/download_helper.php b/system/helpers/download_helper.php new file mode 100644 index 0000000..a6463df --- /dev/null +++ b/system/helpers/download_helper.php @@ -0,0 +1,158 @@ + 0) + ? @rmdir($path) + : TRUE; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('get_filenames')) +{ + /** + * Get Filenames + * + * Reads the specified directory and builds an array containing the filenames. + * Any sub-folders contained within the specified path are read as well. + * + * @param string path to source + * @param bool whether to include the path as part of the filename + * @param bool internal variable to determine recursion status - do not use in calls + * @return array + */ + function get_filenames($source_dir, $include_path = FALSE, $_recursion = FALSE) + { + static $_filedata = array(); + + if ($fp = @opendir($source_dir)) + { + // reset the array and make sure $source_dir has a trailing slash on the initial call + if ($_recursion === FALSE) + { + $_filedata = array(); + $source_dir = rtrim(realpath($source_dir), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; + } + + while (FALSE !== ($file = readdir($fp))) + { + if (is_dir($source_dir.$file) && $file[0] !== '.') + { + get_filenames($source_dir.$file.DIRECTORY_SEPARATOR, $include_path, TRUE); + } + elseif ($file[0] !== '.') + { + $_filedata[] = ($include_path === TRUE) ? $source_dir.$file : $file; + } + } + + closedir($fp); + return $_filedata; + } + + return FALSE; + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('get_dir_file_info')) +{ + /** + * Get Directory File Information + * + * Reads the specified directory and builds an array containing the filenames, + * filesize, dates, and permissions + * + * Any sub-folders contained within the specified path are read as well. + * + * @param string path to source + * @param bool Look only at the top level directory specified? + * @param bool internal variable to determine recursion status - do not use in calls + * @return array + */ + function get_dir_file_info($source_dir, $top_level_only = TRUE, $_recursion = FALSE) + { + static $_filedata = array(); + $relative_path = $source_dir; + + if ($fp = @opendir($source_dir)) + { + // reset the array and make sure $source_dir has a trailing slash on the initial call + if ($_recursion === FALSE) + { + $_filedata = array(); + $source_dir = rtrim(realpath($source_dir), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; + } + + // Used to be foreach (scandir($source_dir, 1) as $file), but scandir() is simply not as fast + while (FALSE !== ($file = readdir($fp))) + { + if (is_dir($source_dir.$file) && $file[0] !== '.' && $top_level_only === FALSE) + { + get_dir_file_info($source_dir.$file.DIRECTORY_SEPARATOR, $top_level_only, TRUE); + } + elseif ($file[0] !== '.') + { + $_filedata[$file] = get_file_info($source_dir.$file); + $_filedata[$file]['relative_path'] = $relative_path; + } + } + + closedir($fp); + return $_filedata; + } + + return FALSE; + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('get_file_info')) +{ + /** + * Get File Info + * + * Given a file and path, returns the name, path, size, date modified + * Second parameter allows you to explicitly declare what information you want returned + * Options are: name, server_path, size, date, readable, writable, executable, fileperms + * Returns FALSE if the file cannot be found. + * + * @param string path to file + * @param mixed array or comma separated string of information returned + * @return array + */ + function get_file_info($file, $returned_values = array('name', 'server_path', 'size', 'date')) + { + if ( ! file_exists($file)) + { + return FALSE; + } + + if (is_string($returned_values)) + { + $returned_values = explode(',', $returned_values); + } + + foreach ($returned_values as $key) + { + switch ($key) + { + case 'name': + $fileinfo['name'] = basename($file); + break; + case 'server_path': + $fileinfo['server_path'] = $file; + break; + case 'size': + $fileinfo['size'] = filesize($file); + break; + case 'date': + $fileinfo['date'] = filemtime($file); + break; + case 'readable': + $fileinfo['readable'] = is_readable($file); + break; + case 'writable': + $fileinfo['writable'] = is_really_writable($file); + break; + case 'executable': + $fileinfo['executable'] = is_executable($file); + break; + case 'fileperms': + $fileinfo['fileperms'] = fileperms($file); + break; + } + } + + return $fileinfo; + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('get_mime_by_extension')) +{ + /** + * Get Mime by Extension + * + * Translates a file extension into a mime type based on config/mimes.php. + * Returns FALSE if it can't determine the type, or open the mime config file + * + * Note: this is NOT an accurate way of determining file mime types, and is here strictly as a convenience + * It should NOT be trusted, and should certainly NOT be used for security + * + * @param string $filename File name + * @return string + */ + function get_mime_by_extension($filename) + { + static $mimes; + + if ( ! is_array($mimes)) + { + $mimes = get_mimes(); + + if (empty($mimes)) + { + return FALSE; + } + } + + $extension = strtolower(substr(strrchr($filename, '.'), 1)); + + if (isset($mimes[$extension])) + { + return is_array($mimes[$extension]) + ? current($mimes[$extension]) // Multiple mime types, just give the first one + : $mimes[$extension]; + } + + return FALSE; + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('symbolic_permissions')) +{ + /** + * Symbolic Permissions + * + * Takes a numeric value representing a file's permissions and returns + * standard symbolic notation representing that value + * + * @param int $perms Permissions + * @return string + */ + function symbolic_permissions($perms) + { + if (($perms & 0xC000) === 0xC000) + { + $symbolic = 's'; // Socket + } + elseif (($perms & 0xA000) === 0xA000) + { + $symbolic = 'l'; // Symbolic Link + } + elseif (($perms & 0x8000) === 0x8000) + { + $symbolic = '-'; // Regular + } + elseif (($perms & 0x6000) === 0x6000) + { + $symbolic = 'b'; // Block special + } + elseif (($perms & 0x4000) === 0x4000) + { + $symbolic = 'd'; // Directory + } + elseif (($perms & 0x2000) === 0x2000) + { + $symbolic = 'c'; // Character special + } + elseif (($perms & 0x1000) === 0x1000) + { + $symbolic = 'p'; // FIFO pipe + } + else + { + $symbolic = 'u'; // Unknown + } + + // Owner + $symbolic .= (($perms & 0x0100) ? 'r' : '-') + .(($perms & 0x0080) ? 'w' : '-') + .(($perms & 0x0040) ? (($perms & 0x0800) ? 's' : 'x' ) : (($perms & 0x0800) ? 'S' : '-')); + + // Group + $symbolic .= (($perms & 0x0020) ? 'r' : '-') + .(($perms & 0x0010) ? 'w' : '-') + .(($perms & 0x0008) ? (($perms & 0x0400) ? 's' : 'x' ) : (($perms & 0x0400) ? 'S' : '-')); + + // World + $symbolic .= (($perms & 0x0004) ? 'r' : '-') + .(($perms & 0x0002) ? 'w' : '-') + .(($perms & 0x0001) ? (($perms & 0x0200) ? 't' : 'x' ) : (($perms & 0x0200) ? 'T' : '-')); + + return $symbolic; + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('octal_permissions')) +{ + /** + * Octal Permissions + * + * Takes a numeric value representing a file's permissions and returns + * a three character string representing the file's octal permissions + * + * @param int $perms Permissions + * @return string + */ + function octal_permissions($perms) + { + return substr(sprintf('%o', $perms), -3); + } +} diff --git a/system/helpers/form_helper.php b/system/helpers/form_helper.php new file mode 100644 index 0000000..aa7379f --- /dev/null +++ b/system/helpers/form_helper.php @@ -0,0 +1,1032 @@ +config->site_url($CI->uri->uri_string()); + } + // If an action is not a full URL then turn it into one + elseif (strpos($action, '://') === FALSE) + { + $action = $CI->config->site_url($action); + } + + $attributes = _attributes_to_string($attributes); + + if (stripos($attributes, 'method=') === FALSE) + { + $attributes .= ' method="post"'; + } + + if (stripos($attributes, 'accept-charset=') === FALSE) + { + $attributes .= ' accept-charset="'.strtolower(config_item('charset')).'"'; + } + + $form = '
\n"; + + // Add CSRF field if enabled, but leave it out for GET requests and requests to external websites + if ($CI->config->item('csrf_protection') === TRUE && strpos($action, $CI->config->base_url()) !== FALSE && ! stripos($form, 'method="get"')) + { + $hidden[$CI->security->get_csrf_token_name()] = $CI->security->get_csrf_hash(); + } + + if (is_array($hidden)) + { + foreach ($hidden as $name => $value) + { + $form .= ''."\n"; + } + } + + return $form; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_open_multipart')) +{ + /** + * Form Declaration - Multipart type + * + * Creates the opening portion of the form, but with "multipart/form-data". + * + * @param string the URI segments of the form destination + * @param array a key/value pair of attributes + * @param array a key/value pair hidden data + * @return string + */ + function form_open_multipart($action = '', $attributes = array(), $hidden = array()) + { + if (is_string($attributes)) + { + $attributes .= ' enctype="multipart/form-data"'; + } + else + { + $attributes['enctype'] = 'multipart/form-data'; + } + + return form_open($action, $attributes, $hidden); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_hidden')) +{ + /** + * Hidden Input Field + * + * Generates hidden fields. You can pass a simple key/value string or + * an associative array with multiple values. + * + * @param mixed $name Field name + * @param string $value Field value + * @param bool $recursing + * @return string + */ + function form_hidden($name, $value = '', $recursing = FALSE) + { + static $form; + + if ($recursing === FALSE) + { + $form = "\n"; + } + + if (is_array($name)) + { + foreach ($name as $key => $val) + { + form_hidden($key, $val, TRUE); + } + + return $form; + } + + if ( ! is_array($value)) + { + $form .= '\n"; + } + else + { + foreach ($value as $k => $v) + { + $k = is_int($k) ? '' : $k; + form_hidden($name.'['.$k.']', $v, TRUE); + } + } + + return $form; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_input')) +{ + /** + * Text Input Field + * + * @param mixed + * @param string + * @param mixed + * @return string + */ + function form_input($data = '', $value = '', $extra = '') + { + $defaults = array( + 'type' => 'text', + 'name' => is_array($data) ? '' : $data, + 'value' => $value + ); + + return '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_password')) +{ + /** + * Password Field + * + * Identical to the input function but adds the "password" type + * + * @param mixed + * @param string + * @param mixed + * @return string + */ + function form_password($data = '', $value = '', $extra = '') + { + is_array($data) OR $data = array('name' => $data); + $data['type'] = 'password'; + return form_input($data, $value, $extra); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_upload')) +{ + /** + * Upload Field + * + * Identical to the input function but adds the "file" type + * + * @param mixed + * @param string + * @param mixed + * @return string + */ + function form_upload($data = '', $value = '', $extra = '') + { + $defaults = array('type' => 'file', 'name' => ''); + is_array($data) OR $data = array('name' => $data); + $data['type'] = 'file'; + + return '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_textarea')) +{ + /** + * Textarea field + * + * @param mixed $data + * @param string $value + * @param mixed $extra + * @return string + */ + function form_textarea($data = '', $value = '', $extra = '') + { + $defaults = array( + 'name' => is_array($data) ? '' : $data, + 'cols' => '40', + 'rows' => '10' + ); + + if ( ! is_array($data) OR ! isset($data['value'])) + { + $val = $value; + } + else + { + $val = $data['value']; + unset($data['value']); // textareas don't use the value attribute + } + + return '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_multiselect')) +{ + /** + * Multi-select menu + * + * @param string + * @param array + * @param mixed + * @param mixed + * @return string + */ + function form_multiselect($name = '', $options = array(), $selected = array(), $extra = '') + { + $extra = _attributes_to_string($extra); + if (stripos($extra, 'multiple') === FALSE) + { + $extra .= ' multiple="multiple"'; + } + + return form_dropdown($name, $options, $selected, $extra); + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('form_dropdown')) +{ + /** + * Drop-down Menu + * + * @param mixed $data + * @param mixed $options + * @param mixed $selected + * @param mixed $extra + * @return string + */ + function form_dropdown($data = '', $options = array(), $selected = array(), $extra = '') + { + $defaults = array(); + + if (is_array($data)) + { + if (isset($data['selected'])) + { + $selected = $data['selected']; + unset($data['selected']); // select tags don't have a selected attribute + } + + if (isset($data['options'])) + { + $options = $data['options']; + unset($data['options']); // select tags don't use an options attribute + } + } + else + { + $defaults = array('name' => $data); + } + + is_array($selected) OR $selected = array($selected); + is_array($options) OR $options = array($options); + + // If no selected state was submitted we will attempt to set it automatically + if (empty($selected)) + { + if (is_array($data)) + { + if (isset($data['name'], $_POST[$data['name']])) + { + $selected = array($_POST[$data['name']]); + } + } + elseif (isset($_POST[$data])) + { + $selected = array($_POST[$data]); + } + } + + $extra = _attributes_to_string($extra); + + $multiple = (count($selected) > 1 && stripos($extra, 'multiple') === FALSE) ? ' multiple="multiple"' : ''; + + $form = '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_checkbox')) +{ + /** + * Checkbox Field + * + * @param mixed + * @param string + * @param bool + * @param mixed + * @return string + */ + function form_checkbox($data = '', $value = '', $checked = FALSE, $extra = '') + { + $defaults = array('type' => 'checkbox', 'name' => ( ! is_array($data) ? $data : ''), 'value' => $value); + + if (is_array($data) && array_key_exists('checked', $data)) + { + $checked = $data['checked']; + + if ($checked == FALSE) + { + unset($data['checked']); + } + else + { + $data['checked'] = 'checked'; + } + } + + if ($checked == TRUE) + { + $defaults['checked'] = 'checked'; + } + else + { + unset($defaults['checked']); + } + + return '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_radio')) +{ + /** + * Radio Button + * + * @param mixed + * @param string + * @param bool + * @param mixed + * @return string + */ + function form_radio($data = '', $value = '', $checked = FALSE, $extra = '') + { + is_array($data) OR $data = array('name' => $data); + $data['type'] = 'radio'; + + return form_checkbox($data, $value, $checked, $extra); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_submit')) +{ + /** + * Submit Button + * + * @param mixed + * @param string + * @param mixed + * @return string + */ + function form_submit($data = '', $value = '', $extra = '') + { + $defaults = array( + 'type' => 'submit', + 'name' => is_array($data) ? '' : $data, + 'value' => $value + ); + + return '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_reset')) +{ + /** + * Reset Button + * + * @param mixed + * @param string + * @param mixed + * @return string + */ + function form_reset($data = '', $value = '', $extra = '') + { + $defaults = array( + 'type' => 'reset', + 'name' => is_array($data) ? '' : $data, + 'value' => $value + ); + + return '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_button')) +{ + /** + * Form Button + * + * @param mixed + * @param string + * @param mixed + * @return string + */ + function form_button($data = '', $content = '', $extra = '') + { + $defaults = array( + 'name' => is_array($data) ? '' : $data, + 'type' => 'button' + ); + + if (is_array($data) && isset($data['content'])) + { + $content = $data['content']; + unset($data['content']); // content is not an attribute + } + + return '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_label')) +{ + /** + * Form Label Tag + * + * @param string The text to appear onscreen + * @param string The id the label applies to + * @param array Additional attributes + * @return string + */ + function form_label($label_text = '', $id = '', $attributes = array()) + { + + $label = ' 0) + { + foreach ($attributes as $key => $val) + { + $label .= ' '.$key.'="'.$val.'"'; + } + } + + return $label.'>'.$label_text.''; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_fieldset')) +{ + /** + * Fieldset Tag + * + * Used to produce
text. To close fieldset + * use form_fieldset_close() + * + * @param string The legend text + * @param array Additional attributes + * @return string + */ + function form_fieldset($legend_text = '', $attributes = array()) + { + $fieldset = '\n"; + if ($legend_text !== '') + { + return $fieldset.''.$legend_text."\n"; + } + + return $fieldset; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_fieldset_close')) +{ + /** + * Fieldset Close Tag + * + * @param string + * @return string + */ + function form_fieldset_close($extra = '') + { + return '
'.$extra; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_close')) +{ + /** + * Form Close Tag + * + * @param string + * @return string + */ + function form_close($extra = '') + { + return ''.$extra; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_prep')) +{ + /** + * Form Prep + * + * Formats text so that it can be safely placed in a form field in the event it has HTML tags. + * + * @deprecated 3.0.0 An alias for html_escape() + * @param string|string[] $str Value to escape + * @return string|string[] Escaped values + */ + function form_prep($str) + { + return html_escape($str, TRUE); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('set_value')) +{ + /** + * Form Value + * + * Grabs a value from the POST array for the specified field so you can + * re-populate an input field or textarea. If Form Validation + * is active it retrieves the info from the validation class + * + * @param string $field Field name + * @param string $default Default value + * @param bool $html_escape Whether to escape HTML special characters or not + * @return string + */ + function set_value($field, $default = '', $html_escape = TRUE) + { + $CI =& get_instance(); + + $value = (isset($CI->form_validation) && is_object($CI->form_validation) && $CI->form_validation->has_rule($field)) + ? $CI->form_validation->set_value($field, $default) + : $CI->input->post($field, FALSE); + + isset($value) OR $value = $default; + return ($html_escape) ? html_escape($value) : $value; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('set_select')) +{ + /** + * Set Select + * + * Let's you set the selected value of a