@@ -0,0 +1,3 @@
+{
+ "directory": "public/assets/vendor"
+}
@@ -0,0 +1,12 @@
+public/assets/packtacular/
+CCF/app/config/migrator.json
+CCF/vendor/
+storage/
+.DS_Store
+Thumbs.db
+composer.phar
+composer.lock
+phpunit.xml
+phpunit.phar
+report/
+run
@@ -0,0 +1,26 @@
+language: php
+
+php:
+ - 5.3
+ - 5.4
+ - 5.5
+ - 5.6
+ - hhvm
+env:
+ - DB=mysql
+before_script:
+ - mysql -e 'create database ccf2_phpunit_application'
+ - mysql -e 'create database ccf2_phpunit_database'
+ - composer self-update
+ - composer install --prefer-source --no-interaction --dev
+script:
+ - php cli phpunit::build
+ - php cli doctor::security_key
+ - phpunit --coverage-text
+matrix:
+ allow_failures:
+ - php: hhvm
@@ -0,0 +1,72 @@
+<?php
+/*
+ *---------------------------------------------------------------
+ * Application Object
+ *
+ * This is your default application object.
+ */
+class App extends CCApp
+ /**
+ * The application name
+ * @var string
+ public static $name = 'My Application';
+ * App configuration
+ * @var CCConfig
+ public static $config = null;
+ * current user object
+ * @var User
+ public static $user = null;
+ * Application initialization.
+ * do your inital stuff here like getting the current user object ect..
+ * You can return a CCResponse wich will cancle all other actions
+ * if enebaled ( see. main.config -> send_app_wake_response )
+ * @return void | CCResponse
+ public static function wake()
+ {
+ /*
+ * Start the session by adding the current uri
+ //CCSession::set( 'uri', CCServer::uri() );
+ * try to authenticate the user
+ //static::$user =& CCAuth::handler()->user;
+ * load the App configuration
+ //static::$config = CCConfig::create( 'app' );
+ }
+ * Environment wake
+ * This is an environment wake they get called if your running
+ * the application in a special environment like:
+ * wake_production
+ * wake_phpunit
+ * wake_test
+ public static function wake_development()
+ CCProfiler::check( 'App::development - Application finished wake events.' );
@@ -0,0 +1,35 @@
+/**
+ * User model
+ **
+ * @package ClanCatsFramework
+ * @author Mario Döring <[email protected]>
+ * @version 2.0
+ * @copyright 2010 - 2014 ClanCats GmbH
+class User extends Auth\User
+ * The users table
+ protected static $_table = 'auth_users';
+ * The user model defaults
+ * @var array
+ protected static $_defaults = array(
+ 'id' ,
+ 'active' => array( 'bool', true ),
+ 'group_id' => 0,
+ 'email' => null,
+ 'password' => null,
+ 'storage' => array( 'json', array() ),
+ 'last_login' => array( 'timestamp', 0 ),
+ 'created_at' => array( 'timestamp' ),
+ 'modified_at' => array( 'timestamp' ),
+ );
@@ -0,0 +1,15 @@
+ * This is your main application configuration file
+return array(
+ * an description of your application
+ 'description' => 'ClanCats Framework, because your time is precious. HMVC PHP framework.',
+);
@@ -0,0 +1,17 @@
+ * Auth configuration
+ * This is the default configuration for the main session
+ 'main' => array(
+ // The User model class
+ 'user_model' => "\\User",
+ ),
@@ -0,0 +1,24 @@
+ * Database configuration
+ * the default database
+ // selected database
+ 'db' => 'localhost',
+ // driver
+ 'driver' => 'mysql',
+ // auth
+ 'host' => '127.0.0.1',
+ 'user' => 'root',
+ 'pass' => '',
+ 'charset' => 'utf8'
@@ -0,0 +1,27 @@
+ * Main CCF configuration
+ * Take a look at the core main configuration file to see
+ * what options are around. .../CCF/vendor/clancats/core/config/main.config.php
+ * URL configuration
+ 'url' => array(
+ // if not in the root directory set the path offset.
+ 'path' => '/',
+ * Security
+ 'security' => array(
+ // it is really important that you choose your own one!
+ 'salt' => '7Q[„YI[œ1<-2S3Ck[%¼Sz59vQ!sl1aœÃ',
@@ -0,0 +1,40 @@
+ * Router map configuration
+ * You can use this configuration to set you default application
+ * routes.
+ * JSON response benchmark
+ 'json' => function()
+ return CCResponse::create( json_encode(
+ array('message' => 'Hello, World!')
+ ), 200 )->header( 'Content-Type', 'application/json' );
+ },
+ 'db' => function()
+ $queries = CCIn::get( 'queries', 1 );
+ $worlds = array();
+ for($i = 0; $i < $queries; ++$i)
+ $worlds[] = DB::select( 'World' )->find( mt_rand(1, 10000) );
+ return CCResponse::create( json_encode( $worlds ) )
+ ->header( 'Content-Type', 'application/json' );
+ 'fortunes' => 'Bench@fortunes',
@@ -0,0 +1,78 @@
+<?php namespace CCConsole; use CCCli;
+ * App console
+ * stuff to maintain your application
+class app extends \CCConsoleController {
+ * return an array with information about the script
+ public function help()
+ return array(
+ 'name' => 'App',
+ 'desc' => 'stuff to maintain your application',
+ 'actions' => array(
+ 'info' => 'show information about this application.',
+ * print information about this application
+ * @param array $params
+ public function action_info( $params )
+ $app = \ClanCats::runtime();
+ // print the app name
+ $this->line( \CCForge_Php::make( 'comment',
+ $app::$name.PHP_EOL.
+ "*".PHP_EOL.
+ "Running on ClanCatsFramework ".\ClanCats::VERSION.PHP_EOL.
+ "© 2010 - ".date('Y')." ClanCats GmbH".PHP_EOL
+ ), 'cyan' );
+ // list printer
+ $list = function( $array )
+ foreach( $array as $key => $value )
+ $this->line( $key.': '.CCCli::color( $value, 'green' ) );
+ };
+ // print info
+ $list( array(
+ 'Runtime Class' => $app,
+ 'CCF Version' => \ClanCats::version(),
+ 'CCF Environment' => \ClanCats::environment(),
+ 'Development env' => var_export( \ClanCats::in_development(), true ),
+ 'File extention' => EXT,
+ 'Core namespace' => CCCORE_NAMESPACE,
+ ));
+ // paths
+ $this->line( PHP_EOL."Paths", 'cyan' );
+ $list( \ClanCats::paths() );
+ $this->line( PHP_EOL."Directories", 'cyan' );
+ $list( \ClanCats::directories() );
+ // autoloader
+ $this->line( PHP_EOL."Autoloader - namespaces", 'cyan' );
+ $list( \CCFinder::$namespaces );
@@ -0,0 +1,137 @@
+ * Basic Authentication Controller
+ * This is an example that should help to explain how CCAuth
+ * and a basic controller works. Feel free to edit.
+class AuthController extends CCViewController
+ * Sign in action
+ * @return CCResponse
+ public function action_sign_in()
+ // Here we set the page topic seen in the title tag
+ $this->theme->topic = __( ':action.topic' );
+ // lets assign the view. Instead of getting the view directly
+ // using CCView::create( 'path/to/view' ) we get the view from the
+ // theme this allows us to have a diffrent sign_in for every theme.
+ // If the view does not exist in the theme it will load the view from
+ // the default view folder.
+ $this->view = $this->theme->view( 'auth/sign_in.view' );
+ $this->view->last_identifier = CCIn::post( 'identifier' );
+ // By checking the HTTP method we figure out if this is a post request or not.
+ if ( CCIn::method( 'post' ) )
+ // Validate the data and get the user object.
+ // We use the key "identifier" because you can configure on
+ // what fields the user is able to login. You could add for example
+ // the username or the customer number etc.
+ if ( $user = CCAuth::validate( CCIn::post( 'identifier' ), CCIn::post( 'password' ) ) )
+ // sign in the user with the current session.
+ CCAuth::sign_in( $user );
+ // flash a success message to the user that he has been
+ // logged in succesfully.
+ UI\Alert::flash( 'success', __( ':action.message.success' ) );
+ // Redirect the user back to the url where he came from
+ // this will only work when the next get parameter is set.
+ return CCRedirect::next();
+ // If we could not recive a user object the login data were clearly invalid.
+ UI\Alert::add( 'danger', __( ':action.message.invalid' ) );
+ * Sign up action
+ public function action_sign_up()
+ // When the user is already authenticated we redirect him home.
+ if ( CCAuth::valid() )
+ return CCRedirect::to( '/' );
+ $this->view = $this->theme->view( 'auth/sign_up.view' );
+ // create a new user object as data holder
+ $user = new User;
+ // bind the newly created user object to our view
+ $this->view->bind( 'user', $user );
+ // Lets assign the email and the password to our
+ // user object using the stirct assign method wich
+ // will ignore all other post values in the assing process.
+ $user->strict_assign( array( 'email', 'password' ), CCIn::all( 'post' ) );
+ $validator = CCValidator::post();
+ // assign the labels to the validator this way we get
+ // correct translated error messages.
+ $validator->label( array(
+ 'email' => __( 'model/user.label.email' ),
+ 'password' => __( 'model/user.label.password' ),
+ 'password_match' => __( 'model/user.label.password_match' )
+ // does the user already exist
+ $validator->set( 'same_email', User::find( 'email', $user->email ) );
+ $validator->message( __(':action.message.email_in_use'), 'negative', 'same_email' );
+ // validate the other fields
+ $validator->rules( 'email', 'required', 'email' );
+ $validator->rules( 'password', 'required', 'min:6' );
+ $validator->rules( 'password_match', 'required', 'match:password' );
+ // when the data passes the validation
+ if ( $validator->success() )
+ // because the user input is correct we can now save the
+ // object to the database and sign the user in.
+ $user->save();
+ else
+ UI\Alert::add( 'danger', $validator->errors() );
+ * Sign out action
+ public function action_sign_out()
+ if ( !CCSession::valid_fingerprint() )
+ CCAuth::sign_out(); return CCRedirect::to( '/' );
@@ -0,0 +1,33 @@
+class BenchController extends CCController
+ public function action_fortunes()
+ $view = CCView::create( 'bench/fortune' );
+ $fortunes = DB::select( 'Fortune' )->run();
+ $runtimeFortune = new stdClass;
+ $runtimeFortune->id = 0;
+ $runtimeFortune->message = 'Additional fortune added at request time.';
+ $fortunes[] = $runtimeFortune;
+ usort($fortunes, function($left, $right) {
+ if ($left->message === $right->message) {
+ return 0;
+ } else if ($left->message > $right->message) {
+ return 1;
+ } else {
+ return -1;
+ });
+ $view->fortunes = $fortunes;
+ return CCResponse::create( $view->render() );
@@ -0,0 +1,29 @@
+ * ErrorController
+ * @package CCFApplication
+ * @version 1.0.0
+class ErrorController extends \CCController
+ * Not found 404
+ public function action_404()
+ return CCResponse::create( CCView::create( 'Core::CCF/404' )->render(), 404 );
+ * Server error 500
+ public function action_500()
+ return CCResponse::create( CCView::create( 'Core::CCF/500' )->render(), 500 );
@@ -0,0 +1,34 @@
+ * Welcome Controller
+class WelcomeController extends CCViewController
+ * The default theme
+ * if you wish you can render this controller with a special theme
+ protected $_theme = null;
+ * the index function is just "function <controller_name>Index() {}"
+ public function action_index()
+ $this->theme->topic = "Willkommen";
+ $this->view = $this->theme->view( 'welcome', array(
+ 'runtime_name' => ClanCats::runtime( 'name' ),
+ 'environment' => ClanCats::environment(),
@@ -0,0 +1,10 @@
+<?php return array(
+ 'sign_in.topic' => 'Einloggen',
+ 'sign_in.message.invalid' => 'Der Benutzername und das Passwort stimmen nicht überein.',
+ 'sign_in.message.success' => 'Willkommen zurück!',
+ 'sign_up.topic' => 'Registrieren',
+ 'sign_up.submit' => 'Konto Erstellen',
+ 'sign_up.message.email_in_use' => 'Diese E-Mail Adresse wird bereits verwendet.',
+ 'sign_up.message.success' => 'Ihr Konto wurde erfolgreich erstellt.',
@@ -0,0 +1,7 @@
+ 'label.name' => 'Name',
+ 'label.email' => 'Email',
+ 'label.password' => 'Passwort',
+ 'label.password_match' => 'Wiederholen',
+ 'label.retain' => 'Eingeloggt bleiben',
+ 'sign_in.topic' => 'Sign In',
+ 'sign_in.message.invalid' => 'Wrong email and password combination.',
+ 'sign_in.message.success' => 'Welcome back!',
+ 'sign_up.topic' => 'Sign Up',
+ 'sign_up.submit' => 'Create my account',
+ 'sign_up.message.email_in_use' => 'This Email address is already in use.',
+ 'sign_up.message.success' => 'Your account has been successfully created.',
+ 'label.password' => 'Password',
+ 'label.password_match' => 'Repeat',
+ 'label.retain' => 'Remember me',
@@ -0,0 +1,18 @@
+ "name": "Twitter Bootstrap with packtacular",
+ "version": "3.0.3",
+ "description": "Sleek, intuitive, and powerful mobile first front-end framework for faster and easier web development.",
+ "homepage": "http://getbootstrap.com",
+ "keywords": [
+ "Twitter",
+ "Theme"
+ ],
+ "license": "MIT",
+ "authors": [],
+ "namespace": "Bootstrap",
+ "wake": false,
+ "install": false,
+ "uninstall": false
@@ -0,0 +1,36 @@
+<?php namespace Bootstrap;
+ * Bootstrap Theme
+ * @package BootstrapTheme
+class Theme extends \Packtacular\Theme
+ * Returns the current view namespace
+ * You need to implement this method in your theme subclass.
+ * @return string
+ public static function view_namespace()
+ return __NAMESPACE__;
+ * This the current public namespace
+ * By default its simply the PUBLICPATH/assets/<theme_namespace>
+ public static function public_namespace()
+ return "assets/".__NAMESPACE__.'/';
@@ -0,0 +1,65 @@
+ * CCFTheme default configuration
+ 'default' => array(
+ * the topic gets added to the title
+ 'topic' => 'no title',
+ * the html title template
+ 'title' => '%s | '.ClanCats::runtime( 'name' ),
+ * the default html description
+ 'description' => 'Change your default description under theme.config -> defatul.description.',
+ * sidebar ( if false full container gets used )
+ 'sidebar' => false,
+ * Footer appended scripts
+ * When you have a view specifc script you can append them to the js var just like:
+ * $theme->capture_append( 'js', function() {
+ * echo "<script>console.log( 'hello world' );</script>";
+ * });
+ 'js' => '',
+ * Assets configuration
+ * you can pack files to gether:
+ * <holder>@<pack>
+ 'assets' => array(
+ // load bootstrap core
+ 'css/bootstrap.min.css' => 'theme@style',
+ 'css/style.css' => 'theme@style',
+ // add mixins
+ 'less/mixins/mixins.less' => 'theme@style',
+ 'less/mixins/background.less' => 'theme@style',
+ 'less/mixins/css3.less' => 'theme@style',
+ 'less/mixins/transform.less' => 'theme@style',
+ // Main style
+ 'less/style.less' => 'theme@style',
+ // js core
+ 'jquery.js' => 'vendor@lib',
+ 'js/bootstrap.min.js' => 'theme@core',
+ 'js/application.js' => 'theme@app',
+ )
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="<?php echo ClanCats::$config->charset; ?>">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title><?php echo $title; ?></title>
+ <meta name="description" content="<?php echo $description; ?>">
+ <!-- styling -->
+ <?php echo CCAsset::code( 'css', 'vendor' ); ?>
+ <?php echo CCAsset::code( 'css', 'theme' ); ?>
+ <?php echo CCAsset::code( 'css' ); ?>
+ <!--[if lt IE 9]>
+ <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
+ <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
+ <![endif]-->
+ <!-- header scripts -->
+ <?php echo CCAsset::code( 'js', 'header' ); ?>
+</head>
+<body>
+<div id="header">
+ <nav class="navbar navbar-inverse navbar-static-top" role="navigation">
+ <div class="container">
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#ccf-navbar">
+ <span class="sr-only">Toggle navigation</span>
+ <span class="icon-bar"></span>
+ </button>
+ <a class="navbar-brand" href="<?php echo to('/'); ?>"><?php echo App::name(); ?></a>
+ </div>
+ <div class="collapse navbar-collapse" id="ccf-navbar">
+ <ul class="nav navbar-nav">
+ <?php if ( CCAuth::valid() ) : ?>
+ <li>
+ <a href="<?php echo to( '@auth.sign_out' ); ?>">Sign Out</a>
+ </li>
+ <?php else : ?>
+ <a href="<?php echo to( '@auth.sign_in' ); ?>">Sign In</a>
+ <a href="<?php echo to( '@auth.sign_up' ); ?>">Sign Up</a>
+ <?php endif; ?>
+ <?php if ( ClanCats::in_development() ) : ?>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">Dev Toolbox <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="<?php echo to('dev/session/'); ?>">Session Sandbox</a></li>
+ <li><a href="<?php echo to('dev/mail/'); ?>">Mail Sandbox</a></li>
+ <li><a href="<?php echo to('dev/common/phpinfo'); ?>">PHP Info</a></li>
+ </ul>
+ </div><!-- /.navbar-collapse -->
+ </div><!-- /.container-fluid -->
+ </nav>
+</div><!-- /#header -->
+<div id="main-container" class="container">
+ <?php echo UI\Alert::render(); ?>
+ <div>
+ <?php if ( $sidebar !== false ) : ?>
+ <div class="row">
+ <div class="col-md-3">
+ <?php echo $sidebar; ?>
+ <div class="col-md-9">
+ <?php echo $content; ?>
+</div>
+<!-- footer scripts -->
+<?php echo CCAsset::code( 'js', 'vendor' ); ?>
+<?php echo CCAsset::code( 'js', 'theme' ); ?>
+<?php echo CCAsset::code( 'js' ); ?>
+<?php echo $js; ?>
+</body>
+</html>
+ "name": "Twitter Bootstrap",
@@ -0,0 +1,81 @@
+class Theme extends \CCTheme
+ * the theme configuration
+ * static theme init
+ * @return void
+ public static function _init()
+ static::$config = \CCConfig::create( __NAMESPACE__.'::theme' );
+ * custom theme render stuff
+ * @param string $file
+ public function render( $file = null )
+ foreach( static::$config->default as $key => $value )
+ $this->set( $key, $value );
+ // assign the topic to the title
+ if ( $this->has( 'topic' ) && $this->has( 'title' ) )
+ $this->title = sprintf( $this->get( 'title' ), $this->get( 'topic' ) );
+ // add default assets
+ \CCAsset::add( 'js/jquery/jquery.js' );
+ \CCAsset::add( 'js/bootstrap.min.js', 'theme' );
+ \CCAsset::add( 'css/bootstrap.min.css', 'theme' );
+ \CCAsset::add( 'css/style.css', 'theme' );
+ return parent::render( $file );
@@ -0,0 +1,45 @@
+ <head>
+ </head>
+ <body>
+ <div id="main-container" class="container">
+ <!-- footer scripts -->
+ <?php echo CCAsset::code( 'js', 'lib' ); ?>
+ <?php echo CCAsset::code( 'js', 'theme' ); ?>
+ <?php echo CCAsset::code( 'js' ); ?>
+ </body>
@@ -0,0 +1,48 @@
+{% use UI\Form; %}
+<div class="row">
+ <div class="col-md-6 col-md-offset-3">
+ <div class="col-sm-offset-2 col-sm-10">
+ <h2>{{__( ':action.topic' )}}</h2>
+ <!-- sing in form -->
+ {{ Form::start( 'sign-in', array( 'method' => 'post', 'class' => 'form-horizontal' ) ) }}
+ <!-- identifier -->
+ <div class="form-group">
+ {{ Form::label( 'identifier', __( 'model/user.label.email' ) )->add_class( 'col-sm-2' ) }}
+ <div class="col-sm-10">
+ {{ Form::input( 'identifier', $last_identifier )
+ ->placeholder( __( 'model/user.label.email' ) ) }}
+ <!-- password -->
+ {{ Form::label( 'password', __( 'model/user.label.password' ) )->add_class( 'col-sm-2' ) }}
+ {{ Form::input( 'password', null, 'password' )
+ ->placeholder( __( 'model/user.label.password' ) ) }}
+ <!-- remember me -->
+ {{ Form::checkbox( 'retain', __( 'model/user.label.retain' ), true ) }}
+ <!-- buttons -->
+ <button type="submit" class="btn btn-primary">{{__( ':action.topic' )}}</button>
+ {{ Form::end(); }}
@@ -0,0 +1,50 @@
+ {{ Form::label( 'email', __( 'model/user.label.email' ) )->add_class( 'col-sm-2' ); }}
+ {{ Form::input( 'email', $user->email, 'email' )
+ ->placeholder( __( 'model/user.label.email' ) ); }}
+ {{ Form::label( 'password', __( 'model/user.label.password' ) )->add_class( 'col-sm-2' ); }}
+ ->placeholder( __( 'model/user.label.password' ) ); }}
+ <!-- password match -->
+ {{ Form::label( 'password_match', __( 'model/user.label.password_match' ) )->add_class( 'col-sm-2' ); }}
+ {{ Form::input( 'password_match', null, 'password' )
+ ->placeholder( __( 'model/user.label.password_match' ) ); }}
+ <button type="submit" class="btn btn-primary">{{__( ':action.submit' )}}</button>
+ {{ Form::end() }}
+ <title>Fortunes</title>
+<table>
+ <tr>
+ <th>id</th>
+ <th>message</th>
+ </tr>
+ <?php foreach($fortunes as $fortune): ?>
+ <td><?php echo $fortune->id; ?></td>
+ <td><?php echo $fortune->message; ?></td>
+ <?php endforeach; ?>
+</table>
@@ -0,0 +1,6 @@
+<h1>Wilkommen bei CCF 2.0</h1>
+<strong>Running: </strong><?php echo $runtime_name; ?>
+<br>
+<strong>Environment: </strong><?php echo $environment; ?>
+<hr>
+<?php namespace Auth;
+ * Auth interface
+class CCAuth
+ * Get an auth handler
+ * @param string $name The auth instance
+ * @return bool
+ public static function handler( $name = null )
+ return Handler::create( $name );
+ * Check if the login is valid
+ public static function valid( $name = null )
+ return Handler::create( $name )->valid();
+ * Validate user credentials
+ * @param mixed $identifier
+ * @param string $password
+ public static function validate( $identifier, $password, $name = null )
+ return Handler::create( $name )->validate( $identifier, $password );
+ * Sign in a user
+ * @param Auth\User $user
+ * @param string $keep_loggedin
+ public static function sign_in( \Auth\User $user, $keep_loggedin = true, $name = null )
+ return Handler::create( $name )->sign_in( $user, $keep_loggedin );
+ * Sign out a user
+ * @return false
+ public static function sign_out( $name = null )
+ return Handler::create( $name )->sign_out();
+ * Auth Exception
+class Exception extends \Core\CCException {}
@@ -0,0 +1,39 @@
+ * Group
+class Group extends \DB\Model
+ * The database table name
+ protected static $_table = 'auth_groups';
+ * Let the model automatically set created_at and modified_at
+ * @var bool
+ protected static $_timestamps = true;
+ * The default properties
+ 'id',
+ 'name' => array( 'string', '' ),
+ 'created_at' => array( 'int', 0 ),
+ 'modified_at' => array( 'int', 0 )
@@ -0,0 +1,476 @@
+ * Auth instnace handler
+use Core\CCCookie;
+class Handler
+ * Instance holder
+ protected static $_instances = array();
+ * Default auth instance name
+ private static $_default = 'main';
+ * Get an auth instance or create one
+ * @param string $name
+ * @param array $conf You can pass optionally a configuration directly. This will overwrite.
+ * @return Auth_Handler
+ public static function create( $name = null, $conf = null )
+ if ( is_null( $name ) )
+ $name = static::$_default;
+ if ( !is_null( $conf ) && is_array( $conf ) )
+ return static::$_instances[$name] = new static( $name, $conf );
+ if ( !isset( static::$_instances[$name] ) )
+ static::$_instances[$name] = new static( $name );
+ return static::$_instances[$name];
+ * Kill an instance to force the handler to redo the construction
+ public static function kill_instance( $name )
+ if ( array_key_exists( $name, static::$_instances ) )
+ unset( static::$_instances[$name] );
+ * the user object
+ * @var DB\Model
+ public $user = null;
+ * is the instance authenticated
+ protected $authenticated = false;
+ * The auth handler name
+ protected $name = null;
+ * The auth config array
+ protected $config = null;
+ * The used session manager
+ public $session = null;
+ * Auth instance constructor
+ * @param array $config
+ public function __construct( $name, $config = null )
+ if ( is_null( $config ) )
+ $config = \CCConfig::create( 'auth' )->get( $name );
+ // check for an alias. If you set a string
+ // in your config file we use the config
+ // with the passed key.
+ if ( is_string( $config ) )
+ $config = \CCConfig::create( 'auth' )->get( $config );
+ if ( !is_array( $config ) )
+ throw new Exception( "Auth\\Handler::create - Invalid auth handler (".$name.")." );
+ // also don't forget to set the name manager name becaue we need him later.
+ $this->name = $name;
+ // assign defaults and create the configuration object
+ $this->config = \CCDataObject::assign( \CCArr::merge( array(
+ // Wich session manager should be used?
+ // null = default session manager
+ 'session_manager' => null,
+ // On wich field should the current logged in user
+ // id be saved in the session?
+ 'session_key' => 'user_id',
+ // On wich field do we select the user for
+ // the authentification
+ 'user_key' => 'id',
+ 'user_model' => "\\Auth\\User",
+ // the identifiers wich fields should be allowed
+ // to select the user object on validation.
+ 'identifiers' => array(
+ 'email'
+ // Where to store the active logins
+ // how long do they stay active etc.
+ 'logins' => array(
+ // the logins db handlerw
+ 'handler' => null,
+ // the logins db table
+ 'table' => 'auth_logins',
+ // login restoring settings
+ 'restore' => array(
+ // the user id key cookie name
+ 'id_cookie' => 'ccauth-restore-id',
+ // the user restore token cookie name
+ 'token_cookie' => 'ccauth-restore-token',
+ // the restore key lifetime
+ 'lifetime' => \CCDate::months(1),
+ ), $config ));
+ // set the session handler
+ $this->session = \CCSession::manager( $this->config->session_manager );
+ $user_model = $this->config->user_model;
+ // set a empty default user object to avoid
+ // on a non object errors
+ $this->user = new $user_model;
+ // do we already have a user id means are we
+ // logged in?
+ if ( !is_null( $session_key = $this->session_user_id() ) )
+ if ( $user = $user_model::find( $this->config->user_key, $session_key ) )
+ $this->user = $user; return $this->authenticated = true;
+ // When no session key / user id is given try to restore
+ // the login using the login keepers
+ $restore_id_cookie = $this->config->get( 'restore.id_cookie' );
+ $restore_token_cookie = $this->config->get( 'restore.token_cookie' );
+ if
+ (
+ CCCookie::has( $restore_id_cookie ) &&
+ CCCookie::has( $restore_token_cookie )
+ // get the restore cookies
+ $restore_id = CCCookie::get( $restore_id_cookie );
+ $restore_token = CCCookie::get( $restore_token_cookie );
+ // get the restore login
+ $login = $this->select_logins()
+ ->where( 'restore_id', $restore_id )
+ ->where( 'restore_token', $restore_token )
+ ->limit( 1 );
+ // if no login found kill the cookies and return
+ if ( !$login = $login->run() )
+ $this->kill_restore();
+ return $this->authenticated = false;
+ // Invalid user? kill the cookies and return
+ if ( !$user = $user_model::find( $this->config->user_key, $restore_id ) )
+ // validate the restore key if invalid
+ // once again kill the cookies and return
+ if ( $login->restore_token != $this->restore_key( $user ) )
+ // If everything is fine sign the user in and
+ // update the restore keys
+ $this->sign_in( $user, true );
+ return $this->authenticated = true;
+ * Kill the restore keys
+ public function kill_restore()
+ CCCookie::delete( $this->config->get( 'restore.id_cookie' ) );
+ CCCookie::delete( $this->config->get( 'restore.token_cookie' ) );
+ * Is this login valid?
+ public function valid()
+ return $this->authenticated;
+ * Get the current user session user id
+ * @return mixed
+ public function session_user_id()
+ return $this->session->get( $this->config->session_key );
+ * generate the current restore key
+ * @param User $user
+ public function restore_key( $user )
+ return \CCStr::hash( $user->password.'@'.$user->{$this->config->user_key}.'%'.\CCIn::client()->ip );
+ * Select from logins
+ * @return DB\Query_Select
+ private function select_logins()
+ return \DB::select(
+ $this->config->get( 'logins.table' ),
+ array(),
+ $this->config->get( 'logins.handler' )
+ * Get the current login of the user
+ * @return stdObject|null
+ public function login()
+ return $this->select_logins()
+ ->where( 'restore_id', $this->user->{$this->config->user_key} )
+ ->where( 'restore_token', $this->restore_key( $this->user ) )
+ ->limit( 1 )
+ ->run();
+ * Validate an identifier with the password
+ * In other words is the login correct?
+ * @param string $identifier
+ * @return mixed false on failure, user object on success
+ public function validate( $identifier, $password )
+ $user = null;
+ foreach( $this->config->identifiers as $property )
+ if ( $user = $user_model::find( $property, $identifier ) )
+ break;
+ // when could not find a user matching the identifiers return false
+ if ( !$user )
+ return false;
+ // when the passwords match return the user object
+ if ( \CCStr::verify_hash( $password, $user->password ))
+ return $user;
+ // otherwise return false
+ * Sign the user and optinal also set the resore keys
+ * @param bool $keep_login
+ public function sign_in( \Auth\User $user, $keep_login = true )
+ // set the session key so the session knows we are logged in
+ $this->session->set( $this->config->session_key, $user->{$this->config->user_key} );
+ // update the current user object
+ $this->user = $user;
+ // update the last login timestamp
+ $this->user->last_login = time();
+ // pass the user trough the events to allow modifications
+ // of the user object at sign in
+ $this->user = \CCEvent::pass( 'auth.sign_in', $this->user );
+ // save the user object to the database
+ $this->user->save();
+ // set the restore keys to keep the login
+ // after the session ends
+ if ( $keep_login )
+ $restore_lifetime = $this->config->get( 'restore.lifetime' );
+ $restore_id = $this->session->get( $this->config->session_key );
+ $restore_token = $this->restore_key( $this->user );
+ CCCookie::set( $restore_id_cookie, $restore_id, $restore_lifetime );
+ CCCookie::set( $restore_token_cookie, $restore_token, $restore_lifetime );
+ // try to get the current login
+ ->where( 'restore_token', $restore_token );
+ // prepare the login data
+ $login_data = array(
+ 'restore_id' => $restore_id,
+ 'restore_token' => $restore_token,
+ 'last_login' => time(),
+ 'client_agent' => \CCIn::client()->agent,
+ // pass the login data trough the events
+ $login_data = \CCEvent::pass( 'auth.store_login', $login_data );
+ // if there is no such login create a new one
+ if ( !$login->run() )
+ \DB::insert( $this->config->get( 'logins.table' ), $login_data )
+ ->run( $this->config->get( 'logins.handler' ) );
+ \DB::update( $this->config->get( 'logins.table' ), $login_data )
+ // and finally we are authenticated
+ * Sign a user out
+ * @param id $user_id
+ public function sign_out() {
+ if ( !$this->authenticated )
+ // remove the restore login
+ \DB::delete( $this->config->get( 'logins.table' ) )
+ // logout the user
+ $this->session->delete( $this->config->session_key );
+ // pass the user object through all user hooks
+ $this->user = \CCEvent::pass( 'auth.sign_out', $this->user );
+ // create new empty user
@@ -0,0 +1,84 @@
+class User extends \DB\Model
+ * Hidden fields
+ protected static $_hidden = array( 'password' );
+ * Get the user group
+ * @return \Auth\Group
+ public function group()
+ return $this->belongs_to( '\\Auth\\Group' );
+ * Is the user an administrator?
+ * We simply check the group_id in our default configuration
+ * the user id of the admin group is 100.
+ public function is_admin()
+ return $this->group_id == 100;
+ * Always hash the passwort
+ protected function _set_modifier_password( $password )
+ return \CCStr::hash( $password );
+ * The email should only contain lower case characters
+ * @param string $email
+ protected function _set_modifier_email( $email )
+ return strtolower( $email );
@@ -0,0 +1,409 @@
+<?php namespace DB;
+ * The Query builder
+class Builder
+ * The query parameters
+ public $parameters = array();
+ * The escape pattern escapes table column names etc.
+ * select * from `table`...
+ protected $escape_pattern = '`%s`';
+ * Clear all set parameters
+ public function clear_parameters()
+ $this->parameters = array();
+ * Adds a parameter to the builder
+ public function add_parameter( $value )
+ $this->parameters[] = $value;
+ * creates an parameter and adds it
+ * @param mixed $value
+ public function param( $value )
+ if ( !\DB::is_expression( $value ) )
+ $this->add_parameter( $value ); return '?';
+ return $value;
+ * Filters the parameters removes the keys and Expressions
+ * @param array $parameters
+ * @return array
+ public function filter_parameters( $parameters )
+ return array_values( array_filter( $parameters, function( $item ) {
+ return !\DB::is_expression( $item );
+ }));
+ * Escape / wrap an string for sql
+ * @param string|Expression $string
+ public function escape( $string )
+ if ( \DB::is_expression( $string ) )
+ return $string->value;
+ // the string might contain an 'as' statement that we wil have to split.
+ if ( strpos( $string, ' as ' ) !== false )
+ $string = explode( ' as ', $string );
+ return $this->escape( trim( $string[0] ) ).' as '. $this->escape( trim( $string[1] ) );
+ // it also might contain dott seperations we have to split
+ if ( strpos( $string, '.' ) !== false )
+ $string = explode( '.', $string );
+ foreach( $string as $key => $item )
+ $string[$key] = $this->escape_string( $item );
+ return implode( '.' , $string );
+ return $this->escape_string( $string );
+ * Escape a single string without checking for as and dots
+ * @param string $string
+ public function escape_string( $string )
+ return sprintf( $this->escape_pattern, $string );
+ * Escape an array of items an seprate them with a comma
+ * @param array $array
+ public function escape_list( $array )
+ foreach( $array as $key => $item )
+ $array[$key] = $this->escape( $item );
+ return implode( ', ', $array );
+ * Escape the table
+ * @param Query $query
+ public function escape_table( &$query )
+ $table = $query->table;
+ if ( is_array( $table ) )
+ reset($table); $table = key($table).' as '.$table[key($table)];
+ return $this->escape( $table );
+ * Convert data to parameters and bind them to the query
+ public function parameterize( $params )
+ foreach( $params as $key => $param )
+ $params[$key] = $this->param( $param );
+ return implode( ', ', $params );
+ * Build an insert query
+ public function compile_insert( &$query )
+ $build = ( $query->ignore ? 'insert ignore' : 'insert' ).' into '.$this->escape_table( $query ).' ';
+ $value_collection = $query->values;
+ // Get the array keys from the first array in the collection.
+ // We use them as insert keys.
+ $build .= '('.$this->escape_list( array_keys( reset( $value_collection ) ) ).') values ';
+ // add the array values.
+ foreach( $value_collection as $values )
+ $build .= '('.$this->parameterize( $values ).'), ';
+ // cut the last comma away
+ return substr( $build, 0, -2 );
+ * Build an update query
+ public function compile_update( &$query )
+ $build = 'update '.$this->escape_table( $query ).' set ';
+ foreach( $query->values as $key => $value )
+ $build .= $this->escape( $key ).' = '.$this->param( $value ).', ';
+ $build = substr( $build, 0, -2 );
+ $build .= $this->compile_where( $query );
+ $build .= $this->compile_limit( $query );
+ return $build;
+ * Build an delete query
+ public function compile_delete( &$query )
+ $build = 'delete from '.$this->escape_table( $query );
+ * Build a select
+ public function compile_select( &$query )
+ $build = ( $query->distinct ? 'select distinct' : 'select' ).' ';
+ if ( !empty( $query->fields ) )
+ foreach( $query->fields as $key => $field )
+ if ( !is_numeric( $key ) )
+ $build .= $this->escape( $key ).' as '.$this->escape( $field );
+ elseif ( is_array( $field ) )
+ $build .= $this->escape( $field[0] ).' as '.$this->escape( $field[1] );
+ $build .= $this->escape( $field );
+ $build .= ', ';
+ $build .= '*';
+ // append the table
+ $build .= ' from '.$this->escape_table( $query );
+ // build the where stuff
+ $build .= $this->compile_group( $query );
+ $build .= $this->compile_order( $query );
+ $build .= $this->compile_limit_with_offset( $query );
+ * Build the where part
+ public function compile_where( &$query )
+ $build = '';
+ foreach( $query->wheres as $where )
+ // to make nested wheres possible you can pass an closure
+ // wich will create a new query where you can add your nested wheres
+ if ( !isset( $where[2] ) && is_closure( $where[1] ) )
+ $sub_query = new Query;
+ call_user_func( $where[1], $sub_query );
+ $build .= ' '.$where[0].' ( '.substr( $this->compile_where( $sub_query ), 7 ).' )';
+ foreach( $sub_query->parameters as $param )
+ $this->add_parameter( $param );
+ continue;
+ // when we have an array as where values we have
+ // to parameterize them
+ if ( is_array( $where[3] ) )
+ $where[3] = '('.$this->parameterize( $where[3] ).')';
+ $where[3] = $this->param( $where[3] );
+ // we always need to escepe where 1 wich referrs to the key
+ $where[1] = $this->escape( $where[1] );
+ // implode the beauty
+ $build .= ' '.implode( ' ', $where );
+ * Build the limit and offset part
+ public function compile_limit_with_offset( &$query )
+ if ( is_null( $query->limit ) )
+ return "";
+ return ' limit '.( (int) $query->offset ).', '.( (int) $query->limit );
+ public function compile_limit( &$query )
+ return ' limit '.( (int) $query->limit );
+ * Build the order by statement
+ protected function compile_order( &$query )
+ if ( empty( $query->orders ) )
+ return '';
+ $build = " order by ";
+ foreach( $query->orders as $order )
+ $build .= $this->escape( $order[0] ).' '.$order[1].', ';
+ * Build the gorup by statemnet
+ protected function compile_group( &$query )
+ if ( empty( $query->groups ) )
+ return ' group by '.$this->escape_list( $query->groups );
+ * Handler wraps PDO and handles the final queries
+class Builder_Mysql extends Builder
+class Builder_Sqlite extends Builder
@@ -0,0 +1,230 @@
+ * Database interface
+class DB
+ * Connect to a database and return the connection status
+ * @param string $handler
+ * @param bool
+ public static function connect( $handler = null )
+ return Handler::create( $handler )->connected();
+ * Get a database handler
+ public static function handler( $handler = null, $conf = null )
+ return Handler::create( $handler, $conf );
+ * Create an Database expression object wich will not be escaped
+ * @return Expression
+ public static function raw( $value )
+ return new Expression( $value );
+ * check if we have an db espression object
+ * @param string|Expession
+ public static function is_expression( $param )
+ return $param instanceof Expression;
+ * Return the logged queries
+ * @param array
+ public static function query_log()
+ return Handler::log();
+ * Returns the last executed query
+ * @param string
+ public static function last_query()
+ return Handler::last_query();
+ * Fetch data from an sql query
+ * Returns always an array of results
+ * @param string $query
+ public static function fetch( $query, $params = array(), $handler = null, $arguments = array( 'obj' ) )
+ return Handler::create( $handler )->fetch( $query, $params, $arguments );
+ * Run an sql statement will return the number of affected rows
+ public static function run( $query, $params = array(), $handler = null )
+ return Handler::create( $handler )->run( $query, $params );
+ * Select data from the database
+ * @param string $table
+ * @param array $fields
+ public static function select( $table, $fields = array(), $handler = null )
+ return Query::select( $table, $fields, $handler );
+ * Create a select query with an model assignment
+ public static function model( $model, $handler = null )
+ $model_data = call_user_func( $model.'::_model' );
+ return Query::select( $model_data['table'], null, $model_data['handler'] )
+ ->fetch_handler( $model.'::_fetch_handler' );
+ * Find something, means select one record by key
+ * @param int $id
+ * @param string $key
+ public static function find( $table, $id, $key = null, $handler = null )
+ return Query::select( $table )->find( $id, $key, $handler );
+ * Get the first result by key
+ public static function first( $table, $key = null, $handler = null )
+ return Query::select( $table )->first( $key, $handler );
+ * Get the last result by key
+ public static function last( $table, $key = null, $handler = null )
+ return Query::select( $table )->last( $key, $handler );
+ * Just return the count result
+ * @return int
+ public static function count( $table, $handler = null )
+ return Query::select( $table )->count( $handler );
+ * Create an insert query object
+ * @param array $data
+ public static function insert( $table, $values = array(), $handler = null )
+ return Query::insert( $table, $values, $handler );
+ * Create an update
+ * @param array $values
+ * @return DB\Query
+ public static function update( $table, $data = null, $handler = null )
+ return Query::update( $table, $data, $handler );
+ * Create delete query
+ public static function delete( $table, $handler = null )
+ return Query::delete( $table, $handler );
+ * Attention with this one
+ public static function truncate( $table )
+ return DB::run( 'truncate table `'.$table.';' );
+ * Query Exception
@@ -0,0 +1,43 @@
+ * DB Expression
+ * This class is just an value holder so we are able to identify
+ * if a given string should not be escaped.
+class Expression
+ * The value holder
+ public $value = null;
+ * The constructor that assigns our value
+ * @param string $value
+ public function __construct( $value )
+ $this->value = $value;
+ * To string magic in case we want to echo the expression
+ public function __toString()
+ return $this->value;
@@ -0,0 +1,390 @@
+ * Default database instance name
+ * The query log if enabeled
+ private static $_query_log = array();
+ * Static init
+ * When we are in development then we append the qurey log to body
+ * @codeCoverageIgnore
+ if ( \ClanCats::in_development() )
+ // add a hook to the main resposne
+ \CCEvent::mind( 'response.output', function( $output ) {
+ if ( strpos( $output, '</body>' ) === false )
+ return $output;
+ $table = \UI\Table::create( array(
+ 'style' => array(
+ 'width' => '100%',
+ 'cellpadding' => '5',
+ 'class' => 'table debug-table debug-table-db',
+ $table->header( array( '#', 'query' ) );
+ foreach( static::log() as $key => $item )
+ $table->row( array(
+ $key+1,
+ $item
+ return str_replace( '</body>', $table."\n</body>", $output );
+ * Get a database handler instance
+ * The objects get stored on runtime so you can use this function
+ * multipe times wihout reconecting to the database.
+ * @return DB_Handler
+ return static::$_instances[$name] = new static( $conf );
+ * Kill all database connections
+ public static function kill_all_connections()
+ static::$_instances = array();
+ * Get the query log
+ public static function log()
+ return static::$_query_log;
+ return \CCArr::last( static::$_query_log );
+ * The current connection
+ * @var DB\Handler_Driver
+ protected $_driver;
+ * The current query builder
+ * @var DB\Builder
+ protected $_builder;
+ * Are we connected to default Database
+ protected $_connected = false;
+ * Handler constructor and connect to the database
+ protected function __construct( $name )
+ $this->connect( $name );
+ * Get the current driver
+ * @return DB\Handler_Driver
+ public function driver()
+ return $this->_driver;
+ * Set the current driver
+ * @param string $driver The full driver class ( DB\Handler_ExampleDriver )
+ private function set_driver( $driver )
+ $this->_driver = new $driver;
+ * Get the current query builder
+ * @return DB\Builder
+ public function builder()
+ return $this->_builder;
+ * Set the current builder
+ * @param string $driver The full driver class ( DB\Builder_ExampleDriver )
+ private function set_builder( $driver )
+ $this->_builder = new $driver;
+ * Try to etablish a connetion to the database. Also assign the connection
+ * and query builder to the current DB driver.
+ * @param string|array $name When passing an array it will be uesed as configuration.
+ protected function connect( $name )
+ if ( $this->_connected )
+ return true;
+ // check if the name is an array. This way we can
+ // pass the configuration directly. We need this
+ // to create for example an handler without having
+ // the configuration in the database conf file.
+ if ( is_array( $name ) )
+ $config = $name;
+ $config = \CCConfig::create( 'database' )->get( $name );
+ $config = \CCConfig::create( 'database' )->get( $config );
+ // Setup the driver class. We simply use name
+ // from the confif file and make the first letter
+ // capital. example: Handler_Mysql, Handler_Sqlite etc.
+ $driver_class = __NAMESPACE__."\\Handler_".ucfirst( $config['driver'] );
+ if ( !class_exists( $driver_class ) )
+ throw new Exception( "DB\\Handler::connect - The driver (".$driver_class.") is invalid." );
+ $this->set_driver( $driver_class );
+ // setup the builder the same way as the handler.
+ $driver_class = __NAMESPACE__."\\Builder_".ucfirst( $config['driver'] );
+ throw new Exception( "DB\\Handler::connect - The builder (".$driver_class.") is invalid." );
+ $this->set_builder( $driver_class );
+ // finally try to connect the driver with the databse
+ if ( $this->driver()->connect( $config ) )
+ return $this->_connected = true;
+ return $this->_connected = false;
+ * Get the connected status
+ public function connected()
+ return $this->_connected;
+ * Run the query and return the PDO statement
+ public function statement( $query, $params = array() )
+ // we alway prepare the query even if we dont have parameters
+ $sth = $this->driver()->connection()->prepare( $query );
+ // because we set pdo the throw exception on db errors
+ // we catch them here to throw our own exception.
+ try
+ $sth->execute( $params );
+ catch ( \PDOException $e )
+ throw new Exception( "DB\\Handler - PDOException: {$e->getMessage()} \n Query: {$query}" );
+ // In development we alway log the query into an array.
+ $keys = array();
+ foreach ( $params as $key => $value )
+ if ( is_string( $key ) )
+ $keys[] = '/:'.$key.'/';
+ $keys[] = '/[?]/';
+ static::$_query_log[] = preg_replace( $keys, $params, $query, 1 );
+ return $sth;
+ * Run the query and fetch the results
+ * You can pass arguments on the third parameter.
+ * These parameter are just the normal PDO ones but in short.
+ * obj = \PDO::FETCH_OBJ
+ * assoc = \PDO::FETCH_ASSOC
+ public function fetch( $query, $params = array(), $arguments = array( 'obj' ) )
+ $sth = $this->statement( $query, $params );
+ $args = null;
+ foreach( $arguments as $argument )
+ $args |= constant( "\PDO::FETCH_".strtoupper( $argument ) );
+ return $sth->fetchAll( $args );
+ * Run the query and get the correct response
+ * INSERT -> last id
+ * UPDATE -> affected rows
+ * DELETE -> affected rows
+ * etc...
+ public function run( $query, $params = array() )
+ $type = strtolower( substr( $query, 0, strpos( $query, ' ' ) ) );
+ switch ( $type )
+ case 'update':
+ case 'delete':
+ return $sth->rowCount();
+ case 'insert':
+ return $this->driver()->connection()->lastInsertId();
@@ -0,0 +1,83 @@
+class Handler_Driver
+ * the raw connection object
+ * @var \PDO
+ protected $connection = null;
+ * the string used for the PDO connection
+ protected $connection_string = null;
+ * connect to database
+ * @param array $conf
+ public function connect( $conf )
+ $connection_params = array();
+ foreach( $conf as $key => $value )
+ if ( is_string( $value ) )
+ $connection_params[ '{'.$key.'}' ] = $value;
+ $connection_string = \CCStr::replace( $this->connection_string, $connection_params );
+ $this->connection = new \PDO( $connection_string, $conf['user'], $conf['pass'], $this->connection_attributes( $conf ) );
+ // At the moment this will never happen because pdo is going to
+ // trhow an exception when the connection fails
+ /*if ( !$this->connection )
+ }*/
+ // let pdo throw exceptions
+ $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+ * Get the current PDO conncection object
+ * @return \PDO
+ public function connection()
+ return $this->connection;
+ * return the connection attributes
+ protected function connection_attributes( $conf )
+ return array();
+class Handler_Mysql extends Handler_Driver
+ protected $connection_string = 'mysql:host={host};dbname={db}';
+ \PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES ".$conf['charset']
+class Handler_Sqlite extends Handler_Driver
+ protected $connection_string = 'sqlite:{path}';
@@ -0,0 +1,416 @@
+ * Database mirgrations
+class Migrator
+ * The migration conifig file
+ protected static $config = null;
+ * Init the migrator directory
+ \ClanCats::directories( array( 'migration' => 'database/' ) );
+ // read the migration configuration
+ static::$config = \CCConfig::create( 'migrator', 'json' );
+ * Get a migration path by name
+ public static function path( $name, $time = null )
+ if ( is_null( $time ) )
+ $time = time();
+ $namespace = null;
+ if ( strpos( $name, '::' ) !== false )
+ list( $namespace, $name ) = explode( '::', $name );
+ $name = explode( '/', $name );
+ foreach( $name as $key => $value )
+ $name[$key] = \CCStr::clean_url( $value, '_' );
+ $name = implode( '/', $name );
+ return \CCPath::get( ( $namespace ? $namespace.'::' : '' ) .$name.'_'.$time, \ClanCats::directory( 'migration' ), '.sql' );
+ * Run all new migration
+ * @param bool $silent In the silent mode the migrator ignores the migration file
+ public static function migrate( $silent = false )
+ $migrations = $silent ? static::available() : static::unstaged();
+ foreach( $migrations as $key => $value )
+ if ( empty( $value ) )
+ if ( \ClanCats::is_cli() )
+ \CCCli::info( 'found new "'.$key.'" migrations.' );
+ foreach( $value as $time => $path )
+ $migration = new static( $path );
+ // run the migration
+ $migration->up();
+ \CCCli::success( 'migrated '.$migration->name() );
+ static::$config->set( $key.'.revision', $time );
+ if ( !$silent )
+ static::$config->write();
+ * Revert the last migration
+ public static function rollback()
+ // first of all we have to filter only the already migrated versions
+ $available = static::available();
+ foreach( $available as $key => $migrations )
+ foreach( $migrations as $time => $migration )
+ if ( $time > static::$config->get( $key.'.revision', 0 ) )
+ unset( $available[$key][$time] );
+ $revisions = array();
+ foreach( $available as $key => $value )
+ foreach( $value as $name => $path )
+ $revisions[$name.'::'.$key] = $path;
+ // nothing to rollback?
+ if ( empty( $revisions ) )
+ \CCCli::warning( 'nothing to rollback to.' );
+ ksort( $revisions );
+ end( $revisions );
+ list( $time, $key ) = explode( '::', key( $revisions ) );
+ $migration = new static( array_pop( $revisions ) );
+ // rollback the migration
+ $migration->down();
+ // get the lastet migration from the group
+ $others = \CCArr::get( $key, $available );
+ ksort( $others );
+ array_pop( $others );
+ end( $others );
+ // update the config
+ static::$config->set( $key.'.revision', key( $others ) );
+ * The hard reset method deletes all tables from the database
+ * @param string $databse
+ public static function hard_reset( $database = null )
+ $tables = DB::fetch( 'SHOW TABLES', array(), $database, array( 'assoc' ) );
+ foreach( $tables as $table )
+ DB::run( 'DROP TABLE IF EXISTS '.reset( $table ), array(), $database );
+ * Returns the available migrations
+ public static function available()
+ $bundles = array_merge( \CCFinder::$bundles, array(
+ 'app' => \CCPath::get( '', null )
+ $available = array();
+ foreach( $bundles as $name => $path )
+ $directory = $path.\ClanCats::directory( 'migration' );
+ if ( is_dir( $directory ) )
+ $available[strtolower($name)] = static::get_migrations( $directory );
+ return $available;
+ * @param string $path
+ public static function get_migrations( $path )
+ $objects = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $path ), \RecursiveIteratorIterator::SELF_FIRST );
+ $files = array();
+ foreach( $objects as $name => $object )
+ if ( \CCStr::extension( $name ) == 'sql' )
+ $files[\CCStr::cut( substr( basename( $name ), strrpos( basename( $name ), '_' )+1 ), '.' )] = $name;
+ ksort( $files );
+ return $files;
+ * Returns the unstaged migrations based on the configuration
+ public static function unstaged()
+ if ( $time <= static::$config->get( $key.'.revision', 0 ) )
+ * The migration sql file
+ protected $path = null;
+ * The migration name
+ * Creates a new migrator instance
+ * @param string $path The path of the migration
+ protected function __construct( $path )
+ $this->path = $path;
+ $this->name = \CCStr::cut( substr( basename( $path ), 0, strrpos( basename( $path ), '_' ) ), '.' );
+ * Returns the name of the migration
+ public function name()
+ return $this->name;
+ * Migrates the current migration up
+ public function up()
+ \CCCli::info( $this->name.'...', 'migrating' );
+ $this->run_queries( 'up' );
+ * Migrates the current migration down
+ public function down()
+ \CCCli::info( $this->name.'...', 'reverting' );
+ $this->run_queries( 'down' );
+ * Run all queries of specified group
+ * @param string $group
+ private function run_queries( $group )
+ if ( !$handle = fopen( $this->path , "r") )
+ throw new Exception( "Could not read migration: ".$this->path );
+ $mode = null;
+ $query = '';
+ while ( !feof( $handle ) )
+ $buffer = trim( fgets( $handle, 4096 ) );
+ $this->parse_mode( $mode, $buffer );
+ if ( $this->is_comment( $buffer ) )
+ // only continue if we are in up mode
+ if ( $mode === $group )
+ $query .= $buffer;
+ if ( substr( rtrim( $query ), -1 ) == ';' )
+ // run the query
+ DB::run( $query );
+ // reset the query for the next one.
+ fclose($handle);
+ * Try to parse the current mode
+ * @param string $mode
+ * @param string $buffer
+ private function parse_mode( &$mode, $buffer )
+ if ( substr( trim( $buffer ), 0, 6 ) === '# --->' )
+ $mode = trim( substr( $buffer, 6 ) );
+ * Is this string an sql comment?
+ private function is_comment( $buffer )
+ if ( is_string( $buffer ) && strlen( $buffer ) >= 1 )
+ if ( $buffer[0] == '#' )
+ if ( substr( $buffer, 0, 2 ) == '--' )
@@ -0,0 +1,537 @@
+ * DB Model
+class Model extends \CCModel
+ * The table
+ // public static $_table = null;
+ * The primary key
+ // public static $_primary_key = null;
+ * The find mofifier
+ // protected static $_find_modifier = null;
+ // protected static $_timestamps = false;
+ * Prepare the model
+ * @param string $settings The model properties
+ * @param string $class The class name.
+ protected static function _prepare( $settings, $class )
+ $settings = parent::_prepare( $settings, $class );
+ // Next step the table name. If not set we use the
+ // class name appending an 's' in the hope that it
+ // makes sense like Model_User = users
+ if ( property_exists( $class, '_table') )
+ $settings['table'] = static::$_table;
+ $settings['table'] = strtolower( $class );
+ $settings['table'] = explode( "\\", $settings['table'] );
+ $last = array_pop( $settings['table'] );
+ // Often we have these model's in the Model folder, we
+ // don't want this in the table name so we cut it out.
+ if ( substr( $last, 0, strlen( 'model_' ) ) == 'model_' )
+ $last = substr( $last, strlen( 'model_' ) );
+ $settings['table'][] = $last;
+ $settings['table'] = implode( '_', $settings['table'] ).'s';
+ // Next we would like to know the primary key used
+ // in this model for saving, finding etc.. if not set
+ // we use the on configured in the main configuration
+ if ( property_exists( $class, '_primary_key') )
+ $settings['primary_key'] = static::$_primary_key;
+ $settings['primary_key'] = \ClanCats::$config->get( 'database.default_primary_key', 'id' );
+ // Do we should use a special DB handler?
+ if ( property_exists( $class, '_handler') )
+ $settings['handler'] = static::$_handler;
+ $settings['handler'] = null;
+ // The find modifier allows you hijack every find executed
+ // on your model an pass setting's to the query. This allows
+ // you for example to defaultly order by something etc.
+ if ( property_exists( $class, '_find_modifier') )
+ $settings['find_modifier'] = static::$_find_modifier;
+ $settings['find_modifier'] = null;
+ // Enabling this options will set the created_at
+ // and modified at property on save
+ if ( property_exists( $class, '_timestamps') )
+ $settings['timestamps'] = (bool) static::$_timestamps;
+ $settings['timestamps'] = false;
+ return $settings;
+ * Fetch from the databse and created models out of the reults
+ * @param DB\Query_Select $query
+ public static function _fetch_handler( &$query )
+ // because the model is an object we force the fetch
+ // arguments to obj so that we can still make use of
+ // the group by and forward key functions
+ $query->fetch_arguments = array( 'obj' );
+ // Run the query and assign the reults
+ // here we force the fetch arguments to assoc
+ // without this step model::assign will fail
+ return static::assign( $query->handler->fetch( $query->build(), $query->handler->builder()->parameters, array( 'assoc' ) ) );
+ * Returns a query and assign's the current model to it
+ public static function select()
+ return DB::model( get_called_class() );
+ * Model finder
+ * This function allows you direct access to your records.
+ * @param mixed $param
+ * @param mixed $param2
+ * @return CCModel
+ public static function find( $param = null, $param2 = null )
+ $settings = static::_model();
+ $query = DB::select( $settings['table'] );
+ // Do we have a find modifier?
+ if ( !is_null( $settings['find_modifier'] ) )
+ $callbacks = $settings['find_modifier'];
+ if ( !\CCArr::is_collection( $callbacks ) )
+ $callbacks = array( $callbacks );
+ foreach( $callbacks as $call )
+ if ( is_callable( $call ) )
+ call_user_func_array( $call, array( &$query ) );
+ throw new ModelException( "Invalid Callback given to find modifiers." );
+ if ( !is_null( $param ) )
+ // Check if paramert 1 is a valid callback and not a string.
+ // Strings as function callback are not possible because
+ // the user might want to search by key like:
+ // Model::find( 'key', 'type' );
+ if ( is_callable( $param ) && !is_string( $param ) )
+ call_user_func_array( $param, array( &$query ) );
+ // When no param 2 isset we try to find the record by primary key
+ elseif ( is_null( $param2 ) )
+ $query->where( $settings['table'].'.'.$settings['primary_key'], $param )
+ ->limit(1);
+ // When param one and two isset we try to find the record by
+ // the given key and value.
+ elseif ( !is_null( $param2 ) )
+ $query->where( $param, $param2 )
+ // alway group the result
+ $query->forward_key( $settings['primary_key'] );
+ // and we have to fetch assoc
+ $query->fetch_arguments = array( 'assoc' );
+ // and assign
+ return static::assign( $query->run() );
+ * Call a function as a property
+ public function __call_property( $key )
+ $result = parent::__call_property( $key );
+ // when we recive a relation we execute it and save it
+ // to the data to avoid mutlitple queries
+ if ( $result instanceof Model_Relation )
+ return $this->_data_store[$key] = $result->run();
+ return $result;
+ * Has one releationships
+ * Model Car:
+ * function engine()
+ * {
+ * return $this->has_one( 'Car_Engine', 'car_id', 'id' );
+ * }
+ * @param Model $model
+ * @param mixed $foreign_key
+ * @param mixed $key
+ protected function has_one( $model, $foreign_key = null, $local_key = null )
+ return new Model_Relation_HasOne( $this, $model, $foreign_key, $local_key );
+ * Has many releationships
+ * function wheels()
+ * return $this->has_many( 'Car_Wheel', 'car_id', 'id' );
+ protected function has_many( $model, $foreign_key = null, $local_key = null )
+ return new Model_Relation_HasMany( $this, $model, $foreign_key, $local_key );
+ * Belongs To releationships
+ * Model Car_Engine:
+ * function car()
+ * return $this->belongs_to( 'Car', 'id', 'car_id' );
+ protected function belongs_to( $model, $foreign_key = null, $local_key = null )
+ return new Model_Relation_BelongsTo( $this, $model, $foreign_key, $local_key );
+ * find with an relationship
+ * Person::with( 'cars' );
+ * @param array|string $with
+ * @param callback $callback
+ public static function with( $with, $callback = null )
+ if ( !is_array( $with ) )
+ $with = array( $with );
+ // run the callback
+ if ( !is_null( $callback ) )
+ call_user_func_array( $callback, array( &$query ) );
+ // alway group the result and fetch assoc
+ // get the main result set
+ $results = static::assign( $query->run() );
+ $singleton = false;
+ if ( !is_array( $results ) )
+ $results = array( $results );
+ $singleton = true;
+ $ref_object = reset( $results );
+ // we have to sort the relationships to make sure that
+ // select the relations in the right order.
+ asort( $with );
+ foreach( $with as $relation => $callback )
+ if ( is_int( $relation ) && is_string( $callback ) )
+ $relation = $callback;
+ $callback = null;
+ if ( strpos( $relation, '.' ) !== false )
+ $relation_layers = explode( '.', $relation );
+ $relation_name = array_pop( $relation_layers );
+ $relation_collection = array();
+ foreach( $results as $key => &$item )
+ $curr_item = $item;
+ foreach( $relation_layers as $layer )
+ $curr_item = $curr_item->raw( $layer );
+ $relation_collection[] = $curr_item;
+ $ref_object = reset( $relation_collection );
+ $relation_object = call_user_func( array( $ref_object, $relation_name ) );
+ if ( $relation_object instanceof Model_Relation )
+ $relation_object->collection_assign( $relation_name, $relation_collection, $callback );
+ $relation_object = call_user_func( array( $ref_object, $relation ) );
+ $relation_object->collection_assign( $relation, $results, $callback );
+ if ( $singleton )
+ return reset( $results );
+ return $results;
+ * save an model
+ * @param mixed $fields
+ * @return self
+ public function save( $fields = null )
+ // check if we should save just some fields
+ if ( is_null( $fields ) )
+ $fields = array_keys( $settings['defaults'] );
+ elseif ( !is_array( $fields ) )
+ $fields = array( $fields );
+ $pkey = $this->_data_store[$settings['primary_key']];
+ $data = array();
+ // Now we have to filter the data to the save g
+ foreach( $fields as $field )
+ $data[$field] = $this->_data_store[$field];
+ // We have to remove the primary key from our data
+ if ( array_key_exists( $settings['primary_key'], $data ) )
+ unset( $data[$settings['primary_key']] );
+ // We pass the data trough the before save callback.
+ // This is a local callback for performence reasons.
+ $data = $this->_before_save( $data );
+ // after the before save event,
+ // do we have to to something with the data type?
+ foreach( $data as $key => $value )
+ if ( array_key_exists( $key, $settings['types'] ) )
+ $data[$key] = $this->_type_assignment_set( $settings['types'][$key], $value );
+ // check if we have to set the timestamps automatically
+ if ( $settings['timestamps'] === true )
+ if ( array_key_exists( 'created_at', $data ) )
+ // check if created_at should be set
+ if ( $data['created_at'] < 1 )
+ $this->_data_store['created_at'] = $data['created_at'] = time();
+ if ( array_key_exists( 'modified_at', $data ) )
+ $this->_data_store['modified_at'] =$data['modified_at'] = time();
+ // When we already have a primary key we are going to
+ // update our record instead of inserting a new one.
+ if ( !is_null( $pkey ) && $pkey > 0 )
+ $query = DB::update( $settings['table'], $data )
+ ->where( $settings['primary_key'], $pkey );
+ // No primary key? Smells like an insert query.
+ $query = DB::insert( $settings['table'], $data );
+ // We check the query type to handle the response right
+ if ( $query instanceof Query_Insert )
+ $this->_data_store[$settings['primary_key']] = $query->run();
+ $query->run();
+ // after save hookt
+ $this->_after_save();
+ // return self
+ return $this;
+ * Create a copy of the current model
+ * @return DB\Model
+ public function copy()
+ $clone = clone $this; $clone->_data_store[static::_model('primary_key')] = null; return $clone;
+ * Delete the current model from the database
+ public function delete()
+ $result = DB::delete( $settings['table'] )
+ ->where( $settings['primary_key'], $this->_data_store[$settings['primary_key']] )
+ ->limit(1)
+ ->run( $settings['handler'] );
+ $this->_data_store[$cache['primary_key']] = null;
+ * Save data hook
+ * to modify your data before they get saved
+ protected function _before_save( $data ) { return $data; }
+ * After save hook
+ * to modify your data after they get saved
+ protected function _after_save() {}
@@ -0,0 +1,210 @@
+ * DB Model Relation
+class Model_Relation
+ * The current relation query
+ * @var DB\Query
+ public $query = null;
+ * The key on the related model
+ public $foreign_key = null;
+ * The key on the local model
+ public $local_key = null;
+ * The related model
+ public $foreign_model = null;
+ * The model where the request came from
+ public $local_model = null;
+ * Should this relationship deliver a single item
+ public $singleton = true;
+ * Create new relationship instance
+ * @param DB\Model $model
+ * @param string $foreign_key
+ public function __construct( $local, $foreign, $foreign_key, $local_key )
+ $this->local_model = $local;
+ $this->foreign_model = $foreign;
+ if ( !class_exists( $foreign ) )
+ throw new ModelException( 'Invalid class "'.$foreign.'" given for relationship.' );
+ // set or create the for foreign_key
+ if ( !is_null( $foreign_key ) )
+ $this->foreign_key = $foreign_key;
+ $this->foreign_key = $this->get_foreign_key();
+ // set or create the for local_key
+ if ( !is_null( $local_key ) )
+ $this->local_key = $local_key;
+ $this->local_key = $this->get_local_key();
+ // create a basic query objcet
+ $this->query = $this->create_query( $foreign );
+ // prepare the query
+ $this->prepare_query();
+ * Get foreign key from the models
+ protected function get_foreign_key()
+ return basename( $this->local_model->model_settings( 'name' ) ).
+ '_'.$this->local_model->model_settings( 'primary_key' );
+ protected function get_local_key()
+ return call_user_func( $this->foreign_model.'::_model', 'primary_key' );
+ * Creates a query object based on a given model
+ protected function create_query( $model )
+ return call_user_func( $model.'::select' )->forward_key();
+ * Prepare the query object
+ protected function prepare_query() {}
+ * Prepare the query with collection select
+ * @param array $collection
+ protected function collection_query( &$collection )
+ $local_keys = array();
+ foreach( $collection as $item )
+ $local_keys[] = $item->raw( $this->local_key );
+ // set the correct collection where
+ $this->query->wheres = array();
+ $this->query->where( $this->foreign_key, 'in', $local_keys );
+ $this->query->group_result( $this->foreign_key );
+ $this->query->limit( null );
+ * @param string $relation
+ public function collection_assign( $relation, &$collection, $callback = null )
+ // make the query
+ $this->collection_query( $collection );
+ call_user_func_array( $callback, array( &$this->query ) );
+ // get the reults
+ $results = $this->query->run();
+ if ( $this->singleton )
+ $item->raw_set( $relation, reset( $results[ $item->raw( $this->local_key ) ] ) );
+ $item->raw_set( $relation, $results[ $item->raw( $this->local_key ) ] );
+ * Forward the query functionality
+ * @param string $method
+ public function __call( $method, $params )
+ $results = call_user_func_array( array( $this->query, $method ), $params );
+ // because we want to keep the realtionship object
+ // we have to return this if its still the query
+ if ( $results === $this->query )
@@ -0,0 +1,44 @@
+class Model_Relation_BelongsTo extends Model_Relation
+ return basename( call_user_func( $this->foreign_model.'::_model', 'name' ) ).'_'.call_user_func( $this->foreign_model.'::_model', 'primary_key' );
+ protected function prepare_query()
+ $this->query->where( $this->foreign_key, $this->local_model->{$this->local_key} )
@@ -0,0 +1,30 @@
+class Model_Relation_HasMany extends Model_Relation
+ public $singleton = false;
+ $this->query->where( $this->foreign_key, $this->local_model->{$this->local_key} );
+class Model_Relation_HasOne extends Model_Relation
+ * Model Exception
+class ModelException extends Exception {}
@@ -0,0 +1,328 @@
+ * The Query object
+class Query
+ * The default primary key
+ protected static $_default_key = null;
+ static::$_default_key = \ClanCats::$config->get( 'database.default_primary_key', 'id' );
+ * create a new query object with the default params
+ * @return Query
+ protected static function create( $table, $handler = null )
+ return new static( $table, $handler );
+ * Create an insert
+ return Query_Insert::create( $table, $handler )->values( $values );
+ public static function update( $table, $values = array(), $handler = null )
+ return Query_Update::create( $table, $handler )->set( $values );
+ * Create a delete
+ return Query_Delete::create( $table, $handler );
+ return Query_Select::create( $table, $handler )->fields( $fields );
+ * The used table
+ public $table = null;
+ * The handler instance to execute the query
+ * @var Handler
+ public $handler = null;
+ * The query where statements
+ public $wheres = array();
+ * the query offset
+ * @var int
+ public $offset = 0;
+ * the query limit
+ public $limit = null;
+ * Query constructor
+ public function __construct( $table = null, $handler = null )
+ $this->table = $table;
+ $this->handler( $handler );
+ * Set the query table
+ public function table( $table )
+ * Set the query handler
+ * @param string $handler The DB handler instance name
+ public function handler( $handler )
+ $this->handler = Handler::create( $handler );
+ * Create a where statement
+ * where query: <$column> <$param1> <$param2> <$type>
+ * example:
+ * ->where( 'name', 'ladina' ) // name = landina
+ * ->where( 'age', '>', 18 )
+ * ->where( 'name', 'in', array( 'johanna', 'jennifer' ) )
+ * @param string $column The SQL column
+ * @param mixed $param1
+ * @param string $type The where type ( and, or )
+ public function where( $column, $param1 = null, $param2 = null, $type = 'and' )
+ // if this is the first where element we are going to change
+ // the where type to 'where'
+ if ( empty( $this->wheres ) )
+ $type = 'where';
+ // when column is an array we assume to make a bulk and where.
+ // array( 'name' => 'ladina', 'language' => 'de' )
+ // where name = ladina and language = de
+ if ( is_array( $column ) )
+ foreach( $column as $key => $val )
+ $this->where( $key, $val, null, $type );
+ if ( is_closure( $column ) )
+ $this->wheres[] = array( $type, $column ); return $this;
+ // when param2 is null we replace param2 with param one as the
+ // value holder and make param1 to the = operator.
+ if ( is_null( $param2 ) )
+ $param2 = $param1; $param1 = '=';
+ // if the param2 is an array we filter it. Im no more sure why
+ // but it's there since 2 years so i think i had a reason.
+ if ( is_array( $param2 ) )
+ $param2 = array_unique( $param2 );
+ $this->wheres[] = array( $type, $column, $param1, $param2 ); return $this;
+ * Create an or where statement
+ * This is the same as the normal where just with a fixed type
+ public function or_where( $column, $param1 = null, $param2 = null )
+ return $this->where( $column, $param1, $param2, 'or' );
+ * Create an and where statement
+ public function and_where( $column, $param1 = null, $param2 = null )
+ return $this->where( $column, $param1, $param2, 'and' );
+ * Set the query limit
+ * @param int $limit
+ * @param int $limit2
+ public function limit( $limit, $limit2 = null )
+ if ( !is_null( $limit2 ) )
+ $this->offset = (int) $limit;
+ $this->limit = (int) $limit2;
+ $this->limit = $limit;
+ * Create an query limit based on a page and a page size
+ * @param int $page
+ * @param int $size
+ public function page( $page, $size = 25 )
+ // validate page
+ if ( (int) $page < 1 )
+ $page = 1;
+ $this->limit = (int) $size;
+ $this->offset = (int) ( ( $size * $page ) - $size );
+ * Build the query to a string
+ public function build()
+ $this->handler->builder()->clear_parameters();
+ * Build and execute the current query
+ * This wil run the local build function and pass the parameters
+ * from the builder object in the handler
+ public function run( $handler = null )
+ if ( !is_null( $handler ) )
+ return $this->handler->run( $this->build(), $this->handler->builder()->parameters );
@@ -0,0 +1,28 @@
+class Query_Delete extends Query
+ // Some task's like clearing the query parameters
+ // are handeld by the parent build function.
+ parent::build();
+ // Lets run the Builder by passing the current query object
+ return $this->handler->builder()->compile_delete( $this );
@@ -0,0 +1,88 @@
+class Query_Insert extends Query
+ * values container
+ public $values = array();
+ * make an insert ignore
+ public $ignore = false;
+ * Insert ignore setter
+ * @param bool $ignore
+ public function ignore( $ignore = true )
+ $this->ignore = $ignore; return $this;
+ * Add values to the insert
+ public function values( array $values )
+ // do nothing if we get nothing
+ if ( empty( $values ) )
+ // check if the the passed array is a collection.
+ // because we want to be able to insert bulk values.
+ if ( !\CCArr::is_collection( $values ) )
+ $values = array( $values );
+ // because we could recive the arrays in diffrent order
+ // we have to sort them by their key.
+ foreach( $values as $key => $value )
+ ksort( $value ); $values[$key] = $value;
+ // merge the new values with the existing ones.
+ $this->values = array_merge( $this->values, $values );
+ // return self so we can continue running the next function
+ return $this->handler->builder()->compile_insert( $this );
@@ -0,0 +1,436 @@
+class Query_Select extends Query
+ * fields container
+ public $fields = array();
+ * make an distinct
+ public $distinct = false;
+ * order by container
+ public $orders = array();
+ public $groups = array();
+ * group the results
+ * @var false|string
+ public $group_result = false;
+ * forward key
+ public $forward_key = false;
+ * the fetching arguments
+ public $fetch_arguments = array( 'obj' );
+ * the fetching handler
+ * @var callback
+ public $fetch_handler = null;
+ * Distinct select setter
+ public function distinct( $distinct = true )
+ $this->distinct = $distinct; return $this;
+ * Set the select fields
+ public function fields( $fields )
+ if ( !is_array( $fields ) && !is_null( $fields ) )
+ if ( empty( $fields ) || $fields == array( '*' ) )
+ $this->fields = array(); return $this;
+ $this->fields = $fields;
+ * Add select fields
+ public function add_fields( $fields )
+ if ( !is_array( $fields ) )
+ $this->fields = array_merge( $this->fields, $fields );
+ * Set the order parameters
+ * @param mixed $cols
+ * @param string $order
+ public function order_by( $cols, $order = 'asc' )
+ if ( !is_array( $cols ) )
+ $this->orders[] = array( $cols, $order ); return $this;
+ foreach( $cols as $key => $col )
+ if ( is_numeric( $key ) )
+ $this->orders[] = array( $col, $order );
+ $this->orders[] = array( $key, $col );
+ * Add group by stuff
+ * @param mixed $key By passing an array you can add multiple groups
+ public function group_by( $key )
+ if ( !is_array( $key ) )
+ $key = array( $key );
+ foreach( $key as $group_key )
+ $this->groups[] = $group_key;
+ * Forward a result value as array key
+ * @param string|bool $key
+ public function forward_key( $key = true )
+ if ( $key === false )
+ $this->forward_key = false;
+ elseif ( $key === true )
+ $this->forward_key = \ClanCats::$config->get( 'database.default_primary_key', 'id' );
+ $this->forward_key = $key;
+ * Group results by a column
+ * array(
+ * 'name' => 'John', 'age' => 18,
+ * ),
+ * 'name' => 'Jeff', 'age' => 32,
+ * 'name' => 'Jenny', 'age' => 18,
+ * To:
+ * '18' => array(
+ * array( 'name' => 'John', 'age' => 18 ),
+ * array( 'name' => 'Jenny', 'age' => 18 ),
+ * '32' => array(
+ * array( 'name' => 'Jeff', 'age' => 32 ),
+ public function group_result( $key )
+ $this->group_result = false;
+ $this->group_result = $key;
+ * Set the fetch handler for this query
+ public function fetch_handler( $callback )
+ $this->fetch_handler = $callback; return $this;
+ return $this->handler->builder()->compile_select( $this );
+ // if there is a special fetch handler defined pass him all the
+ // needed parameters and retrive the results
+ if ( !is_null( $this->fetch_handler ) )
+ $results = call_user_func_array( $this->fetch_handler, array(
+ &$this
+ // otherwise simply do the default select fetch
+ $results = $this->handler->fetch( $this->build(), $this->handler->builder()->parameters, $this->fetch_arguments );
+ // In case we should forward a key means using a value
+ // from every result as array key.
+ if ( $this->forward_key !== false )
+ $raw_results = $results;
+ $results = array();
+ if ( in_array( 'assoc', $this->fetch_arguments ) )
+ foreach( $raw_results as $result )
+ $results[$result[$this->forward_key]] = $result;
+ $results[$result->{$this->forward_key}] = $result;
+ // Group the results by a value
+ if ( $this->group_result !== false )
+ foreach( $raw_results as $key => $result )
+ $results[$result[$this->group_result]][$key] = $result;
+ $results[$result->{$this->group_result}][$key] = $result;
+ // when the limit is 1 we are going to return the
+ // result directly
+ if ( $this->limit === 1 )
+ * Get one result sets limit to 1 and executes
+ public function one( $name = null )
+ return $this->limit( 0, 1 )->run( $name );
+ public function find( $id, $key = null, $name = null )
+ if ( is_null( $key ) )
+ $key = Query::$_default_key;
+ return $this->where( $key, $id )->one( $name );
+ public function first( $key = null, $name = null )
+ return $this->order_by( $key, 'asc' )->one( $name );
+ public function last( $key = null, $name = null )
+ return $this->order_by( $key, 'desc' )->one( $name );
+ * Just get a single value from the db
+ * @param string $column
+ public function column( $column, $name = null )
+ return $this->fields( $column )->one( $name )->$column;
+ * @param string $db
+ public function count( $name = null )
+ return (int) $this->column( DB::raw( "COUNT(*)" ), $name );
@@ -0,0 +1,64 @@
+class Query_Update extends Query
+ * Add set values to the update query
+ * @param string|array $param1
+ public function set( $param1, $param2 = null )
+ if ( empty( $param1 ) )
+ // when param 2 is not null we assume that only one set is passed
+ // like: set( 'name', 'Lu' ); instead of set( array( 'name' => 'Lu' ) );
+ if ( !is_null( $param2 ) )
+ $param1 = array( $param1 => $param2 );
+ $this->values = array_merge( $this->values, $param1 );
+ return $this->handler->builder()->compile_update( $this );
+class QueryException extends Exception {}
@@ -0,0 +1,105 @@
+ * Database test case
+use CCConfig;
+use CCPath;
+class Test_Case extends \PHPUnit_Framework_TestCase
+ * Is a database configured?
+ public static $dbtests = false;
+ * Return a string of the database config name
+ * used for the test in this class
+ protected static function main_database()
+ // the normal test case uses the app database
+ return 'app';
+ * Check if DB test are possible
+ public static function setUpBeforeClass()
+ $current_datatbase = static::main_database();
+ // lets make sure that we have an db configuration for phpunit
+ if ( CCConfig::create( 'database' )->has( $current_datatbase ) )
+ // lets try to connect to that database if the conection
+ // fails we just return and continue the other tests
+ try { DB::connect( $current_datatbase ); }
+ catch ( \PDOException $e ) { return; }
+ // connection succeeded?
+ static::$dbtests = true;
+ // overwrite the main db
+ CCConfig::create( 'database' )->set( 'main', $current_datatbase );
+ // kill all connections
+ Handler::kill_all_connections();
+ // check for any sql files that should be executed needed
+ // for theses tests. We simply check if a file exists in the
+ // CCUnit bundle "db_<current_database>.sql"
+ if ( file_exists( CCPath::get( 'CCUnit::db_'.$current_datatbase.'.sql' ) ) )
+ $queries = explode( ';', file_get_contents( CCPath::get( 'CCUnit::db_'.$current_datatbase.'.sql' ) ) );
+ foreach( $queries as $query )
+ $query = trim( $query );
+ if ( !empty( $query ) )
+ DB::run( $query, array(), 'phpunit' );
+ public static function tearDownAfterClass()
+ // write the main database back to app
+ CCConfig::create( 'database' )->set( 'main', 'app' );
+ * Check if we can execute db tests
+ * And add a warning to phpunit that we skipped the test.
+ protected function setUp()
+ if ( !static::$dbtests )
+ $this->markTestSkipped( "Warning! Could not connect to phpunit DB. skipping DB unit test." );
@@ -0,0 +1,70 @@
+class Test_Case_Database extends Test_Case
+ * people data bulk
+ public static function people_provider_bulk()
+ array(
+ array( 'name' => 'Mario', 'age' => 20, 'library_id' => 1 ),
+ array( 'name' => 'Ladina', 'age' => 20, 'library_id' => 2 ),
+ array( 'name' => 'Johanna', 'age' => 18, 'library_id' => 1 ),
+ array( 'name' => 'Jenny', 'age' => 22, 'library_id' => 0 ),
+ array( 'name' => 'Melanie', 'age' => 19, 'library_id' => 0 ),
+ array( 'name' => 'Tarek', 'age' => 20, 'library_id' => 3 ),
+ array( 'name' => 'John', 'age' => 42, 'library_id' => 4 ),
+ * people data
+ public static function people_provider()
+ array( 'name' => 'Ladina', 'library_id' => 2, 'age' => 20 ),
+ array( 'name' => 'Johanna', 'age' => -18, 'library_id' => 1 ),
+ array( 'age' => 22, 'library_id' => 0, 'name' => 'Jenny' ),
+ // these tests should use a seperate table
+ return 'phpunit';
@@ -0,0 +1,438 @@
+<?php namespace Mail;
+ * CCMail
+class CCMail
+ * Create a new Mail instance
+ * @param string $transporter
+ * @return CCMail
+ public static function create( $transporter = null, $config = null )
+ return new static( $transporter, $config );
+ * The mail transporter
+ * @var Mail\Transporter
+ protected $transporter = null;
+ * Is this a plaintext email
+ protected $is_plaintext = false;
+ * Mail recipients
+ protected $to = array();
+ * Blind carbon copies
+ protected $bcc = array();
+ * Carbon copies
+ protected $cc = array();
+ * From email and name
+ * @var array[email => name]
+ protected $from = array();
+ * Attachemnts
+ protected $attachments = array();
+ * Mail subject
+ protected $subject = "";
+ * Plaintext message
+ protected $plaintext = null;
+ * HTML Message
+ * @var string|CCView
+ public $message = "";
+ * Mail layout view
+ * @var CCView
+ public $layout = null;
+ * Mail constructor
+ public function __construct( $transporter = null, $config = null )
+ // prepare the transporter
+ $this->transporter = Transporter::create( $transporter, $config );
+ // prepare the layout if we have one
+ if ( $layout = \CCConfig::create( 'mail' )->layout )
+ $this->layout = \CCView::create( $layout );
+ // fill the from with the defaults
+ $this->from = \CCConfig::create( 'mail' )->from;
+ * Add a recipient
+ * $mail->to( '[email protected]' );
+ * $mail->to( '[email protected]', 'Jennifer Rostock' )
+ * $mail->to( array( '[email protected]' => 'Han Solo' ) );
+ public function to( $email, $name = null )
+ if ( !is_array( $email ) )
+ $email = array( $email => $name );
+ foreach( $email as $address => $name )
+ if ( is_numeric( $address ) && is_string( $name ) )
+ $this->to[$name] = null;
+ $this->to[$address] = $name;
+ * Add Blind carbon copies
+ * Works like the 'to' function.
+ public function bcc( $email, $name = null )
+ $this->bcc[$name] = null;
+ $this->bcc[$address] = $name;
+ * Add Carbon copies
+ public function cc( $email, $name = null )
+ $this->cc[$name] = null;
+ $this->cc[$address] = $name;
+ * Set the from email and name
+ * @param string $mail
+ public function from( $email, $name = null )
+ $this->from = array( $email, $name ); return $this;
+ * $mail->attachment( '/path/to/my/file.zip' );
+ * $mail->attachment( '/some/image.jpg', 'your_photo.jpg' );
+ * $mail->attachment( array( '/some/other/image.jpg' => 'wallpaper.jpg' ) );
+ * @param string $path The path to your file
+ public function attachment( $path, $name )
+ if ( !is_array( $path ) )
+ $path = array( $path => $name );
+ foreach( $path as $file => $name )
+ if ( is_numeric( $file ) && is_string( $name ) )
+ $this->attachments[$name] = null;
+ $this->attachments[$file] = $name;
+ * Set the mail subject
+ * @param string $subject
+ public function subject( $subject )
+ $this->subject = $subject; return $this;
+ * Set the mail plaintext message
+ * @param string $plaintext
+ public function plaintext( $plaintext )
+ $this->plaintext = $plaintext; return $this;
+ * Set the mail message
+ * @param string $message
+ public function message( $message )
+ $this->message = $message; return $this;
+ * Set a view as message
+ * @param string $view
+ public function view( $view )
+ $this->message = \CCView::create( $view ); return $this;
+ * Is the current message just plaintext
+ * @param bool $is
+ public function is_plaintext( $is = true )
+ $this->is_plaintext = $is;
+ * Render the message
+ public function render()
+ $message = $this->message;
+ reset( $this->to );
+ // default view parameters for the message and the layout
+ $params = array(
+ 'mail' => $this,
+ 'to_email' => key($this->to),
+ 'to_name' => $this->to[ key($this->to) ],
+ // if the message is a view
+ if ( $message instanceof \CCView )
+ $message->_data = $message->_data + $params;
+ $message = $message->render();
+ // prepare the layout
+ if ( $this->layout )
+ $this->layout->content = $message;
+ $this->layout->_data = $this->layout->_data + $params;
+ $message = $this->layout->render();
+ // return the renderd message
+ return $message;
+ * Prepare the message for sending to the transport
+ public function send()
+ // load the mail configuration
+ $config = \CCConfig::create( 'mail' );
+ // when mailing is disabled do nothing just return
+ if ( $config->disabled === true )
+ return;
+ // we cannot send a mail without recipients
+ if ( empty( $this->to ) )
+ throw new Exception( "Cannot send mail without recipients." );
+ // is a catch all enabled?
+ if ( $config->get( 'catch_all.enabled' ) === true )
+ // to be able to modify the mail without removing
+ // the user options we have to clone the mail
+ $mail = clone $this;
+ // we have to remove all recipients ( to, ccc, bcc ) and set them
+ // to our catch all recipients
+ $mail->to = array();
+ $mail->cc = array();
+ $mail->bcc = array();
+ $mail->to( $config->get( 'catch_all.addresses' ) );
+ // transport the cloned mail
+ return $mail->transport( $config->get( 'catch_all.transporter' ) );
+ // transport the mail
+ $this->transport();
+ * Transport the message
+ * @param string $transport Use a diffrent transporter
+ protected function transport( $transporter = null )
+ if ( !is_null( $transporter ) )
+ $transporter = Transporter::create( $transporter );
+ $transporter = $this->transporter;
+ // pass the current mail to the transporter
+ $transporter->send( $this );
+ * Export the mail data
+ public function export_data()
+ $data = get_object_vars( $this );
+ // render the message
+ $data['message'] = $this->render();
+ unset( $data['transporter'] );
+ return $data;
+ * Mail Exception
@@ -0,0 +1,3417 @@
+<?php namespace Mail\PHPMailer;
+ * PHPMailer - PHP email creation and transport class.
+ * PHP Version 5
+ * @package PHPMailer
+ * @link https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
+ * @author Marcus Bointon (Synchro/coolbru) <[email protected]>
+ * @author Jim Jagielski (jimjag) <[email protected]>
+ * @author Andy Prevost (codeworxtech) <[email protected]>
+ * @author Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2014 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
+ * @note This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+class PHPMailer
+ * The PHPMailer Version number.
+ * @type string
+ public $Version = '5.2.8';
+ * Email priority.
+ * Options: 1 = High, 3 = Normal, 5 = low.
+ * @type integer
+ public $Priority = 3;
+ * The character set of the message.
+ public $CharSet = 'iso-8859-1';
+ * The MIME Content-type of the message.
+ public $ContentType = 'text/plain';
+ * The message encoding.
+ * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable".
+ public $Encoding = '8bit';
+ * Holds the most recent mailer error message.
+ public $ErrorInfo = '';
+ * The From email address for the message.
+ public $From = 'root@localhost';
+ * The From name of the message.
+ public $FromName = 'Root User';
+ * The Sender email (Return-Path) of the message.
+ * If not empty, will be sent via -f to sendmail or as 'MAIL FROM' in smtp mode.
+ public $Sender = '';
+ * The Return-Path of the message.
+ * If empty, it will be set to either From or Sender.
+ * @deprecated Email senders should never set a return-path header;
+ * it's the receiver's job (RFC5321 section 4.4), so this no longer does anything.
+ * @link https://tools.ietf.org/html/rfc5321#section-4.4 RFC5321 reference
+ public $ReturnPath = '';
+ * The Subject of the message.
+ public $Subject = '';
+ * An HTML or plain text message body.
+ * If HTML then call isHTML(true).
+ public $Body = '';
+ * The plain-text message body.
+ * This body can be read by mail clients that do not have HTML email
+ * capability such as mutt & Eudora.
+ * Clients that can read HTML will view the normal Body.
+ public $AltBody = '';
+ * An iCal message part body.
+ * Only supported in simple alt or alt_inline message types
+ * To generate iCal events, use the bundled extras/EasyPeasyICS.php class or iCalcreator
+ * @link http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/
+ * @link http://kigkonsult.se/iCalcreator/
+ public $Ical = '';
+ * The complete compiled MIME message body.
+ * @access protected
+ protected $MIMEBody = '';
+ * The complete compiled MIME message headers.
+ protected $MIMEHeader = '';
+ * Extra headers that createHeader() doesn't fold in.
+ protected $mailHeader = '';
+ * Word-wrap the message body to this number of chars.
+ public $WordWrap = 0;
+ * Which method to use to send mail.
+ * Options: "mail", "sendmail", or "smtp".
+ public $Mailer = 'mail';
+ * The path to the sendmail program.
+ public $Sendmail = '/usr/sbin/sendmail';
+ * Whether mail() uses a fully sendmail-compatible MTA.
+ * One which supports sendmail's "-oi -f" options.
+ * @type boolean
+ public $UseSendmailOptions = true;
+ * Path to PHPMailer plugins.
+ * Useful if the SMTP class is not in the PHP include path.
+ * @deprecated Should not be needed now there is an autoloader.
+ public $PluginDir = '';
+ * The email address that a reading confirmation should be sent to.
+ public $ConfirmReadingTo = '';
+ * The hostname to use in Message-Id and Received headers
+ * and as default HELO string.
+ * If empty, the value returned
+ * by SERVER_NAME is used or 'localhost.localdomain'.
+ public $Hostname = '';
+ * An ID to be used in the Message-Id header.
+ * If empty, a unique id will be generated.
+ public $MessageID = '';
+ * The message Date to be used in the Date header.
+ * If empty, the current date will be added.
+ public $MessageDate = '';
+ * SMTP hosts.
+ * Either a single hostname or multiple semicolon-delimited hostnames.
+ * You can also specify a different port
+ * for each host by using this format: [hostname:port]
+ * (e.g. "smtp1.example.com:25;smtp2.example.com").
+ * Hosts will be tried in order.
+ public $Host = 'localhost';
+ * The default SMTP server port.
+ * @TODO Why is this needed when the SMTP class takes care of it?
+ public $Port = 25;
+ * The SMTP HELO of the message.
+ * Default is $Hostname.
+ * @see PHPMailer::$Hostname
+ public $Helo = '';
+ * The secure connection prefix.
+ * Options: "", "ssl" or "tls"
+ public $SMTPSecure = '';
+ * Whether to use SMTP authentication.
+ * Uses the Username and Password properties.
+ * @see PHPMailer::$Username
+ * @see PHPMailer::$Password
+ public $SMTPAuth = false;
+ * SMTP username.
+ public $Username = '';
+ * SMTP password.
+ public $Password = '';
+ * SMTP auth type.
+ * Options are LOGIN (default), PLAIN, NTLM, CRAM-MD5
+ public $AuthType = '';
+ * SMTP realm.
+ * Used for NTLM auth
+ public $Realm = '';
+ * SMTP workstation.
+ public $Workstation = '';
+ * The SMTP server timeout in seconds.
+ public $Timeout = 10;
+ * SMTP class debug output mode.
+ * Options:
+ * 0: no output
+ * 1: commands
+ * 2: data and commands
+ * 3: as 2 plus connection status
+ * 4: low level data output
+ * @see SMTP::$do_debug
+ public $SMTPDebug = 0;
+ * How to handle debug output.
+ * 'echo': Output plain-text as-is, appropriate for CLI
+ * 'html': Output escaped, line breaks converted to <br>, appropriate for browser output
+ * 'error_log': Output to error log as configured in php.ini
+ * @see SMTP::$Debugoutput
+ public $Debugoutput = 'echo';
+ * Whether to keep SMTP connection open after each message.
+ * If this is set to true then to close the connection
+ * requires an explicit call to smtpClose().
+ public $SMTPKeepAlive = false;
+ * Whether to split multiple to addresses into multiple messages
+ * or send them all in one message.
+ public $SingleTo = false;
+ * Storage for addresses when SingleTo is enabled.
+ * @type array
+ * @TODO This should really not be public
+ public $SingleToArray = array();
+ * Whether to generate VERP addresses on send.
+ * Only applicable when sending via SMTP.
+ * @link http://en.wikipedia.org/wiki/Variable_envelope_return_path
+ * @link http://www.postfix.org/VERP_README.html Postfix VERP info
+ public $do_verp = false;
+ * Whether to allow sending messages with an empty body.
+ public $AllowEmpty = false;
+ * The default line ending.
+ * @note The default remains "\n". We force CRLF where we know
+ * it must be used via self::CRLF.
+ public $LE = "\n";
+ * DKIM selector.
+ public $DKIM_selector = '';
+ * DKIM Identity.
+ * Usually the email address used as the source of the email
+ public $DKIM_identity = '';
+ * DKIM passphrase.
+ * Used if your key is encrypted.
+ public $DKIM_passphrase = '';
+ * DKIM signing domain name.
+ * @example 'example.com'
+ public $DKIM_domain = '';
+ * DKIM private key file path.
+ public $DKIM_private = '';
+ * Callback Action function name.
+ * The function that handles the result of the send email action.
+ * It is called out by send() for each email sent.
+ * Value can be any php callable: http://www.php.net/is_callable
+ * Parameters:
+ * boolean $result result of the send action
+ * string $to email address of the recipient
+ * string $cc cc email addresses
+ * string $bcc bcc email addresses
+ * string $subject the subject
+ * string $body the email body
+ * string $from email address of sender
+ public $action_function = '';
+ * What to use in the X-Mailer header.
+ * Options: null for default, whitespace for none, or a string to use
+ public $XMailer = '';
+ * An instance of the SMTP sender class.
+ * @type SMTP
+ protected $smtp = null;
+ * The array of 'to' addresses.
+ * The array of 'cc' addresses.
+ * The array of 'bcc' addresses.
+ * The array of reply-to names and addresses.
+ protected $ReplyTo = array();
+ * An array of all kinds of addresses.
+ * Includes all of $to, $cc, $bcc, $replyto
+ protected $all_recipients = array();
+ * The array of attachments.
+ protected $attachment = array();
+ * The array of custom headers.
+ protected $CustomHeader = array();
+ * The most recent Message-ID (including angular brackets).
+ protected $lastMessageID = '';
+ * The message's MIME type.
+ protected $message_type = '';
+ * The array of MIME boundary strings.
+ protected $boundary = array();
+ * The array of available languages.
+ protected $language = array();
+ * The number of errors encountered.
+ protected $error_count = 0;
+ * The S/MIME certificate file path.
+ protected $sign_cert_file = '';
+ * The S/MIME key file path.
+ protected $sign_key_file = '';
+ * The S/MIME password for the key.
+ * Used only if the key is encrypted.
+ protected $sign_key_pass = '';
+ * Whether to throw exceptions for errors.
+ protected $exceptions = false;
+ * Error severity: message only, continue processing
+ const STOP_MESSAGE = 0;
+ * Error severity: message, likely ok to continue processing
+ const STOP_CONTINUE = 1;
+ * Error severity: message, plus full stop, critical error reached
+ const STOP_CRITICAL = 2;
+ * SMTP RFC standard line ending
+ const CRLF = "\r\n";
+ * Constructor
+ * @param boolean $exceptions Should we throw external exceptions?
+ public function __construct($exceptions = false)
+ $this->exceptions = ($exceptions == true);
+ // @CCF Fix
+ //Make sure our autoloader is loaded
+ if (version_compare(PHP_VERSION, '5.1.2', '>=')) {
+ $autoload = spl_autoload_functions();
+ if ($autoload === false or !in_array('PHPMailerAutoload', $autoload)) {
+ require 'PHPMailerAutoload.php';
+ * Destructor.
+ public function __destruct()
+ if ($this->Mailer == 'smtp') { //close any open SMTP connection nicely
+ $this->smtpClose();
+ * Call mail() in a safe_mode-aware fashion.
+ * Also, unless sendmail_path points to sendmail (or something that
+ * claims to be sendmail), don't pass params (not a perfect fix,
+ * but it will do)
+ * @param string $to To
+ * @param string $subject Subject
+ * @param string $body Message Body
+ * @param string $header Additional Header(s)
+ * @param string $params Params
+ * @access private
+ * @return boolean
+ private function mailPassthru($to, $subject, $body, $header, $params)
+ //Check overloading of mail function to avoid double-encoding
+ if (ini_get('mbstring.func_overload') & 1) {
+ $subject = $this->secureHeader($subject);
+ $subject = $this->encodeHeader($this->secureHeader($subject));
+ if (ini_get('safe_mode') || !($this->UseSendmailOptions)) {
+ $result = @mail($to, $subject, $body, $header);
+ $result = @mail($to, $subject, $body, $header, $params);
+ * Output debugging info via user-defined method.
+ * Only if debug output is enabled.
+ * @see PHPMailer::$Debugoutput
+ * @see PHPMailer::$SMTPDebug
+ * @param string $str
+ protected function edebug($str)
+ if (!$this->SMTPDebug) {
+ switch ($this->Debugoutput) {
+ case 'error_log':
+ error_log($str);
+ case 'html':
+ //Cleans up output a bit for a better looking display that's HTML-safe
+ echo htmlentities(preg_replace('/[\r\n]+/', '', $str), ENT_QUOTES, $this->CharSet) . "<br>\n";
+ case 'echo':
+ default:
+ echo $str."\n";
+ * Sets message type to HTML or plain.
+ * @param boolean $isHtml True for HTML mode.
+ public function isHTML($isHtml = true)
+ if ($isHtml) {
+ $this->ContentType = 'text/html';
+ $this->ContentType = 'text/plain';
+ * Send messages using SMTP.
+ public function isSMTP()
+ $this->Mailer = 'smtp';
+ * Send messages using PHP's mail() function.
+ public function isMail()
+ $this->Mailer = 'mail';
+ * Send messages using $Sendmail.
+ public function isSendmail()
+ $ini_sendmail_path = ini_get('sendmail_path');
+ if (!stristr($ini_sendmail_path, 'sendmail')) {
+ $this->Sendmail = '/usr/sbin/sendmail';
+ $this->Sendmail = $ini_sendmail_path;
+ $this->Mailer = 'sendmail';
+ * Send messages using qmail.
+ public function isQmail()
+ if (!stristr($ini_sendmail_path, 'qmail')) {
+ $this->Sendmail = '/var/qmail/bin/qmail-inject';
+ $this->Mailer = 'qmail';
+ * Add a "To" address.
+ * @param string $address
+ * @return boolean true on success, false if address already used
+ public function addAddress($address, $name = '')
+ return $this->addAnAddress('to', $address, $name);
+ * Add a "CC" address.
+ * @note: This function works with the SMTP mailer on win32, not with the "mail" mailer.
+ public function addCC($address, $name = '')
+ return $this->addAnAddress('cc', $address, $name);
+ * Add a "BCC" address.
+ public function addBCC($address, $name = '')
+ return $this->addAnAddress('bcc', $address, $name);
+ * Add a "Reply-to" address.
+ public function addReplyTo($address, $name = '')
+ return $this->addAnAddress('Reply-To', $address, $name);
+ * Add an address to one of the recipient arrays.
+ * Addresses that have been added already return false, but do not throw exceptions
+ * @param string $kind One of 'to', 'cc', 'bcc', 'ReplyTo'
+ * @param string $address The email address to send to
+ * @throws phpmailerException
+ * @return boolean true on success, false if address already used or invalid in some way
+ protected function addAnAddress($kind, $address, $name = '')
+ if (!preg_match('/^(to|cc|bcc|Reply-To)$/', $kind)) {
+ $this->setError($this->lang('Invalid recipient array') . ': ' . $kind);
+ $this->edebug($this->lang('Invalid recipient array') . ': ' . $kind);
+ if ($this->exceptions) {
+ throw new phpmailerException('Invalid recipient array: ' . $kind);
+ $address = trim($address);
+ $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
+ if (!$this->validateAddress($address)) {
+ $this->setError($this->lang('invalid_address') . ': ' . $address);
+ $this->edebug($this->lang('invalid_address') . ': ' . $address);
+ throw new phpmailerException($this->lang('invalid_address') . ': ' . $address);
+ if ($kind != 'Reply-To') {
+ if (!isset($this->all_recipients[strtolower($address)])) {
+ array_push($this->$kind, array($address, $name));
+ $this->all_recipients[strtolower($address)] = true;
+ if (!array_key_exists(strtolower($address), $this->ReplyTo)) {
+ $this->ReplyTo[strtolower($address)] = array($address, $name);
+ * Set the From and FromName properties.
+ * @param boolean $auto Whether to also set the Sender address, defaults to true
+ public function setFrom($address, $name = '', $auto = true)
+ $this->From = $address;
+ $this->FromName = $name;
+ if ($auto) {
+ if (empty($this->Sender)) {
+ $this->Sender = $address;
+ * Return the Message-ID header of the last email.
+ * Technically this is the value from the last time the headers were created,
+ * but it's also the message ID of the last sent message except in
+ * pathological cases.
+ public function getLastMessageID()
+ return $this->lastMessageID;
+ * Check that a string looks like an email address.
+ * @param string $address The email address to check
+ * @param string $patternselect A selector for the validation pattern to use :
+ * * `auto` Pick strictest one automatically;
+ * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0, PHP >= 5.3.2, 5.2.14;
+ * * `pcre` Use old PCRE implementation;
+ * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL; same as pcre8 but does not allow 'dotless' domains;
+ * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.
+ * * `noregex` Don't use a regex: super fast, really dumb.
+ * @static
+ * @access public
+ public static function validateAddress($address, $patternselect = 'auto')
+ if (!$patternselect or $patternselect == 'auto') {
+ if (defined('PCRE_VERSION')) { //Check this constant so it works when extension_loaded() is disabled
+ if (version_compare(PCRE_VERSION, '8.0') >= 0) {
+ $patternselect = 'pcre8';
+ $patternselect = 'pcre';
+ //Filter_var appeared in PHP 5.2.0 and does not require the PCRE extension
+ if (version_compare(PHP_VERSION, '5.2.0') >= 0) {
+ $patternselect = 'php';
+ $patternselect = 'noregex';
+ switch ($patternselect) {
+ case 'pcre8':
+ * Uses the same RFC5322 regex on which FILTER_VALIDATE_EMAIL is based, but allows dotless domains.
+ * @link http://squiloople.com/2009/12/20/email-address-validation/
+ * @copyright 2009-2010 Michael Rushton
+ * Feel free to use and redistribute this code. But please keep this copyright notice.
+ return (boolean)preg_match(
+ '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' .
+ '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' .
+ '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' .
+ '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' .
+ '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .
+ '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .
+ '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .
+ '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
+ '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
+ $address
+ case 'pcre':
+ //An older regex that doesn't need a recent PCRE
+ '/^(?!(?>"?(?>\\\[ -~]|[^"])"?){255,})(?!(?>"?(?>\\\[ -~]|[^"])"?){65,}@)(?>' .
+ '[!#-\'*+\/-9=?^-~-]+|"(?>(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\xFF]))*")' .
+ '(?>\.(?>[!#-\'*+\/-9=?^-~-]+|"(?>(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\xFF]))*"))*' .
+ '@(?>(?![a-z0-9-]{64,})(?>[a-z0-9](?>[a-z0-9-]*[a-z0-9])?)(?>\.(?![a-z0-9-]{64,})' .
+ '(?>[a-z0-9](?>[a-z0-9-]*[a-z0-9])?)){0,126}|\[(?:(?>IPv6:(?>(?>[a-f0-9]{1,4})(?>:' .
+ '[a-f0-9]{1,4}){7}|(?!(?:.*[a-f0-9][:\]]){8,})(?>[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,6})?' .
+ '::(?>[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,6})?))|(?>(?>IPv6:(?>[a-f0-9]{1,4}(?>:' .
+ '[a-f0-9]{1,4}){5}:|(?!(?:.*[a-f0-9]:){6,})(?>[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,4})?' .
+ '::(?>(?:[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,4}):)?))?(?>25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
+ '|[1-9]?[0-9])(?>\.(?>25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}))\])$/isD',
+ case 'html5':
+ * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
+ * @link http://www.whatwg.org/specs/web-apps/current-work/#e-mail-state-(type=email)
+ '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .
+ '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
+ case 'noregex':
+ //No PCRE! Do something _very_ approximate!
+ //Check the address is 3 chars or longer and contains an @ that's not the first or last char
+ return (strlen($address) >= 3
+ and strpos($address, '@') >= 1
+ and strpos($address, '@') != strlen($address) - 1);
+ case 'php':
+ return (boolean)filter_var($address, FILTER_VALIDATE_EMAIL);
+ * Create a message and send it.
+ * Uses the sending method specified by $Mailer.
+ * @return boolean false on error - See the ErrorInfo property for details of the error.
+ try {
+ if (!$this->preSend()) {
+ return $this->postSend();
+ } catch (phpmailerException $exc) {
+ $this->mailHeader = '';
+ $this->setError($exc->getMessage());
+ throw $exc;
+ * Prepare a message for sending.
+ public function preSend()
+ if ((count($this->to) + count($this->cc) + count($this->bcc)) < 1) {
+ throw new phpmailerException($this->lang('provide_address'), self::STOP_CRITICAL);
+ // Set whether the message is multipart/alternative
+ if (!empty($this->AltBody)) {
+ $this->ContentType = 'multipart/alternative';
+ $this->error_count = 0; // reset errors
+ $this->setMessageType();
+ // Refuse to send an empty message unless we are specifically allowing it
+ if (!$this->AllowEmpty and empty($this->Body)) {
+ throw new phpmailerException($this->lang('empty_message'), self::STOP_CRITICAL);
+ $this->MIMEHeader = $this->createHeader();
+ $this->MIMEBody = $this->createBody();
+ // To capture the complete message when using mail(), create
+ // an extra header list which createHeader() doesn't fold in
+ if ($this->Mailer == 'mail') {
+ if (count($this->to) > 0) {
+ $this->mailHeader .= $this->addrAppend('To', $this->to);
+ $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');
+ $this->mailHeader .= $this->headerLine(
+ 'Subject',
+ $this->encodeHeader($this->secureHeader(trim($this->Subject)))
+ // Sign with DKIM if enabled
+ if (!empty($this->DKIM_domain)
+ && !empty($this->DKIM_private)
+ && !empty($this->DKIM_selector)
+ && !empty($this->DKIM_domain)
+ && file_exists($this->DKIM_private)) {
+ $header_dkim = $this->DKIM_Add(
+ $this->MIMEHeader . $this->mailHeader,
+ $this->encodeHeader($this->secureHeader($this->Subject)),
+ $this->MIMEBody
+ $this->MIMEHeader = rtrim($this->MIMEHeader, "\r\n ") . self::CRLF .
+ str_replace("\r\n", "\n", $header_dkim) . self::CRLF;
+ * Actually send a message.
+ * Send the email via the selected mechanism
+ public function postSend()
+ // Choose the mailer and send through it
+ switch ($this->Mailer) {
+ case 'sendmail':
+ case 'qmail':
+ return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody);
+ case 'smtp':
+ return $this->smtpSend($this->MIMEHeader, $this->MIMEBody);
+ case 'mail':
+ return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
+ $sendMethod = $this->Mailer.'Send';
+ if (method_exists($this, $sendMethod)) {
+ return $this->$sendMethod($this->MIMEHeader, $this->MIMEBody);
+ $this->edebug($exc->getMessage());
+ * Send mail using the $Sendmail program.
+ * @param string $header The message headers
+ * @param string $body The message body
+ * @see PHPMailer::$Sendmail
+ protected function sendmailSend($header, $body)
+ if ($this->Sender != '') {
+ if ($this->Mailer == 'qmail') {
+ $sendmail = sprintf('%s -f%s', escapeshellcmd($this->Sendmail), escapeshellarg($this->Sender));
+ $sendmail = sprintf('%s -oi -f%s -t', escapeshellcmd($this->Sendmail), escapeshellarg($this->Sender));
+ $sendmail = sprintf('%s', escapeshellcmd($this->Sendmail));
+ $sendmail = sprintf('%s -oi -t', escapeshellcmd($this->Sendmail));
+ if ($this->SingleTo === true) {
+ foreach ($this->SingleToArray as $toAddr) {
+ if (!@$mail = popen($sendmail, 'w')) {
+ throw new phpmailerException($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
+ fputs($mail, 'To: ' . $toAddr . "\n");
+ fputs($mail, $header);
+ fputs($mail, $body);
+ $result = pclose($mail);
+ $this->doCallback(
+ ($result == 0),
+ array($toAddr),
+ $this->cc,
+ $this->bcc,
+ $this->Subject,
+ $body,
+ $this->From
+ if ($result != 0) {
+ $this->doCallback(($result == 0), $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From);
+ * Send mail using the PHP mail() function.
+ * @link http://www.php.net/manual/en/book.mail.php
+ protected function mailSend($header, $body)
+ $toArr = array();
+ foreach ($this->to as $toaddr) {
+ $toArr[] = $this->addrFormat($toaddr);
+ $to = implode(', ', $toArr);
+ $params = ' ';
+ $params = sprintf('-f%s', $this->Sender);
+ if ($this->Sender != '' and !ini_get('safe_mode')) {
+ $old_from = ini_get('sendmail_from');
+ ini_set('sendmail_from', $this->Sender);
+ $result = false;
+ if ($this->SingleTo === true && count($toArr) > 1) {
+ foreach ($toArr as $toAddr) {
+ $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);
+ $this->doCallback($result, array($toAddr), $this->cc, $this->bcc, $this->Subject, $body, $this->From);
+ $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);
+ $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From);
+ if (isset($old_from)) {
+ ini_set('sendmail_from', $old_from);
+ if (!$result) {
+ throw new phpmailerException($this->lang('instantiate'), self::STOP_CRITICAL);
+ * Get an instance to use for SMTP operations.
+ * Override this function to load your own SMTP implementation
+ * @return SMTP
+ public function getSMTPInstance()
+ if (!is_object($this->smtp)) {
+ $this->smtp = new SMTP;
+ return $this->smtp;
+ * Send mail via SMTP.
+ * Returns false if there is a bad MAIL FROM, RCPT, or DATA input.
+ * Uses the PHPMailerSMTP class by default.
+ * @see PHPMailer::getSMTPInstance() to use a different class.
+ * @uses SMTP
+ protected function smtpSend($header, $body)
+ $bad_rcpt = array();
+ if (!$this->smtpConnect()) {
+ throw new phpmailerException($this->lang('smtp_connect_failed'), self::STOP_CRITICAL);
+ $smtp_from = ($this->Sender == '') ? $this->From : $this->Sender;
+ if (!$this->smtp->mail($smtp_from)) {
+ $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError()));
+ throw new phpmailerException($this->ErrorInfo, self::STOP_CRITICAL);
+ // Attempt to send to all recipients
+ foreach ($this->to as $to) {
+ if (!$this->smtp->recipient($to[0])) {
+ $bad_rcpt[] = $to[0];
+ $isSent = false;
+ $isSent = true;
+ $this->doCallback($isSent, array($to[0]), array(), array(), $this->Subject, $body, $this->From);
+ foreach ($this->cc as $cc) {
+ if (!$this->smtp->recipient($cc[0])) {
+ $bad_rcpt[] = $cc[0];
+ $this->doCallback($isSent, array(), array($cc[0]), array(), $this->Subject, $body, $this->From);
+ foreach ($this->bcc as $bcc) {
+ if (!$this->smtp->recipient($bcc[0])) {
+ $bad_rcpt[] = $bcc[0];
+ $this->doCallback($isSent, array(), array(), array($bcc[0]), $this->Subject, $body, $this->From);
+ // Only send the DATA command if we have viable recipients
+ if ((count($this->all_recipients) > count($bad_rcpt)) and !$this->smtp->data($header . $body)) {
+ throw new phpmailerException($this->lang('data_not_accepted'), self::STOP_CRITICAL);
+ if ($this->SMTPKeepAlive == true) {
+ $this->smtp->reset();
+ $this->smtp->quit();
+ $this->smtp->close();
+ if (count($bad_rcpt) > 0) { // Create error message for any bad addresses
+ throw new phpmailerException(
+ $this->lang('recipients_failed') . implode(', ', $bad_rcpt),
+ self::STOP_CONTINUE
+ * Initiate a connection to an SMTP server.
+ * Returns false if the operation failed.
+ * @param array $options An array of options compatible with stream_context_create()
+ public function smtpConnect($options = array())
+ if (is_null($this->smtp)) {
+ $this->smtp = $this->getSMTPInstance();
+ // Already connected?
+ if ($this->smtp->connected()) {
+ $this->smtp->setTimeout($this->Timeout);
+ $this->smtp->setDebugLevel($this->SMTPDebug);
+ $this->smtp->setDebugOutput($this->Debugoutput);
+ $this->smtp->setVerp($this->do_verp);
+ $hosts = explode(';', $this->Host);
+ $lastexception = null;
+ foreach ($hosts as $hostentry) {
+ $hostinfo = array();
+ if (!preg_match('/^((ssl|tls):\/\/)*([a-zA-Z0-9\.-]*):?([0-9]*)$/', trim($hostentry), $hostinfo)) {
+ // Not a valid host entry
+ // $hostinfo[2]: optional ssl or tls prefix
+ // $hostinfo[3]: the hostname
+ // $hostinfo[4]: optional port number
+ // The host string prefix can temporarily override the current setting for SMTPSecure
+ // If it's not specified, the default value is used
+ $prefix = '';
+ $tls = ($this->SMTPSecure == 'tls');
+ if ($hostinfo[2] == 'ssl' or ($hostinfo[2] == '' and $this->SMTPSecure == 'ssl')) {
+ $prefix = 'ssl://';
+ $tls = false; // Can't have SSL and TLS at once
+ } elseif ($hostinfo[2] == 'tls') {
+ $tls = true;
+ // tls doesn't use a prefix
+ $host = $hostinfo[3];
+ $port = $this->Port;
+ $tport = (integer)$hostinfo[4];
+ if ($tport > 0 and $tport < 65536) {
+ $port = $tport;
+ if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) {
+ if ($this->Helo) {
+ $hello = $this->Helo;
+ $hello = $this->serverHostname();
+ $this->smtp->hello($hello);
+ if ($tls) {
+ if (!$this->smtp->startTLS()) {
+ throw new phpmailerException($this->lang('connect_host'));
+ // We must resend HELO after tls negotiation
+ if ($this->SMTPAuth) {
+ if (!$this->smtp->authenticate(
+ $this->Username,
+ $this->Password,
+ $this->AuthType,
+ $this->Realm,
+ $this->Workstation
+ ) {
+ throw new phpmailerException($this->lang('authenticate'));
+ $lastexception = $exc;
+ // We must have connected, but then failed TLS or Auth, so close connection nicely
+ // If we get here, all connection attempts have failed, so close connection hard
+ // As we've caught all exceptions, just report whatever the last one was
+ if ($this->exceptions and !is_null($lastexception)) {
+ throw $lastexception;
+ * Close the active SMTP session if one exists.
+ public function smtpClose()
+ if ($this->smtp !== null) {
+ * Set the language for error messages.
+ * Returns false if it cannot load the language file.
+ * The default language is English.
+ * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr")
+ * @param string $lang_path Path to the language file directory, with trailing separator (slash)
+ public function setLanguage($langcode = 'en', $lang_path = '')
+ // Define full set of translatable strings in English
+ $PHPMAILER_LANG = array(
+ 'authenticate' => 'SMTP Error: Could not authenticate.',
+ 'connect_host' => 'SMTP Error: Could not connect to SMTP host.',
+ 'data_not_accepted' => 'SMTP Error: data not accepted.',
+ 'empty_message' => 'Message body empty',
+ 'encoding' => 'Unknown encoding: ',
+ 'execute' => 'Could not execute: ',
+ 'file_access' => 'Could not access file: ',
+ 'file_open' => 'File Error: Could not open file: ',
+ 'from_failed' => 'The following From address failed: ',
+ 'instantiate' => 'Could not instantiate mail function.',
+ 'invalid_address' => 'Invalid address',
+ 'mailer_not_supported' => ' mailer is not supported.',
+ 'provide_address' => 'You must provide at least one recipient email address.',
+ 'recipients_failed' => 'SMTP Error: The following recipients failed: ',
+ 'signing' => 'Signing Error: ',
+ 'smtp_connect_failed' => 'SMTP connect() failed.',
+ 'smtp_error' => 'SMTP server error: ',
+ 'variable_set' => 'Cannot set or reset variable: '
+ if (empty($lang_path)) {
+ // Calculate an absolute path so it can work if CWD is not here
+ $lang_path = dirname(__FILE__). DIRECTORY_SEPARATOR . 'language'. DIRECTORY_SEPARATOR;
+ $foundlang = true;
+ $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php';
+ if ($langcode != 'en') { // There is no English translation file
+ // Make sure language file path is readable
+ if (!is_readable($lang_file)) {
+ $foundlang = false;
+ // Overwrite language-specific strings.
+ // This way we'll never have missing translations.
+ $foundlang = include $lang_file;
+ $this->language = $PHPMAILER_LANG;
+ return ($foundlang == true); // Returns false if language not found
+ * Get the array of strings for the current language.
+ public function getTranslations()
+ return $this->language;
+ * Create recipient headers.
+ * @param string $type
+ * @param array $addr An array of recipient,
+ * where each recipient is a 2-element indexed array with element 0 containing an address
+ * and element 1 containing a name, like:
+ * array(array('[email protected]', 'Joe User'), array('[email protected]', 'Zoe User'))
+ public function addrAppend($type, $addr)
+ $addresses = array();
+ foreach ($addr as $address) {
+ $addresses[] = $this->addrFormat($address);
+ return $type . ': ' . implode(', ', $addresses) . $this->LE;
+ * Format an address for use in a message header.
+ * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name
+ * like array('[email protected]', 'Joe User')
+ public function addrFormat($addr)
+ if (empty($addr[1])) { // No name provided
+ return $this->secureHeader($addr[0]);
+ return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . ' <' . $this->secureHeader(
+ $addr[0]
+ ) . '>';
+ * Word-wrap message.
+ * For use with mailers that do not automatically perform wrapping
+ * and for quoted-printable encoded messages.
+ * Original written by philippe.
+ * @param string $message The message to wrap
+ * @param integer $length The line length to wrap to
+ * @param boolean $qp_mode Whether to run in Quoted-Printable mode
+ public function wrapText($message, $length, $qp_mode = false)
+ $soft_break = ($qp_mode) ? sprintf(' =%s', $this->LE) : $this->LE;
+ // If utf-8 encoding is used, we will need to make sure we don't
+ // split multibyte characters when we wrap
+ $is_utf8 = (strtolower($this->CharSet) == 'utf-8');
+ $lelen = strlen($this->LE);
+ $crlflen = strlen(self::CRLF);
+ $message = $this->fixEOL($message);
+ if (substr($message, -$lelen) == $this->LE) {
+ $message = substr($message, 0, -$lelen);
+ $line = explode($this->LE, $message); // Magic. We know fixEOL uses $LE
+ $message = '';
+ for ($i = 0; $i < count($line); $i++) {
+ $line_part = explode(' ', $line[$i]);
+ $buf = '';
+ for ($e = 0; $e < count($line_part); $e++) {
+ $word = $line_part[$e];
+ if ($qp_mode and (strlen($word) > $length)) {
+ $space_left = $length - strlen($buf) - $crlflen;
+ if ($e != 0) {
+ if ($space_left > 20) {
+ $len = $space_left;
+ if ($is_utf8) {
+ $len = $this->utf8CharBoundary($word, $len);
+ } elseif (substr($word, $len - 1, 1) == '=') {
+ $len--;
+ } elseif (substr($word, $len - 2, 1) == '=') {
+ $len -= 2;
+ $part = substr($word, 0, $len);
+ $word = substr($word, $len);
+ $buf .= ' ' . $part;
+ $message .= $buf . sprintf('=%s', self::CRLF);
+ $message .= $buf . $soft_break;
+ while (strlen($word) > 0) {
+ if ($length <= 0) {
+ $len = $length;
+ if (strlen($word) > 0) {
+ $message .= $part . sprintf('=%s', self::CRLF);
+ $buf = $part;
+ $buf_o = $buf;
+ $buf .= ($e == 0) ? $word : (' ' . $word);
+ if (strlen($buf) > $length and $buf_o != '') {
+ $message .= $buf_o . $soft_break;
+ $buf = $word;
+ $message .= $buf . self::CRLF;
+ * Find the last character boundary prior to $maxLength in a utf-8
+ * quoted (printable) encoded string.
+ * Original written by Colin Brown.
+ * @param string $encodedText utf-8 QP text
+ * @param integer $maxLength find last character boundary prior to this length
+ * @return integer
+ public function utf8CharBoundary($encodedText, $maxLength)
+ $foundSplitPos = false;
+ $lookBack = 3;
+ while (!$foundSplitPos) {
+ $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack);
+ $encodedCharPos = strpos($lastChunk, '=');
+ if ($encodedCharPos !== false) {
+ // Found start of encoded character byte within $lookBack block.
+ // Check the encoded byte value (the 2 chars after the '=')
+ $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2);
+ $dec = hexdec($hex);
+ if ($dec < 128) { // Single byte character.
+ // If the encoded char was found at pos 0, it will fit
+ // otherwise reduce maxLength to start of the encoded char
+ $maxLength = ($encodedCharPos == 0) ? $maxLength :
+ $maxLength - ($lookBack - $encodedCharPos);
+ $foundSplitPos = true;
+ } elseif ($dec >= 192) { // First byte of a multi byte character
+ // Reduce maxLength to split at start of character
+ $maxLength = $maxLength - ($lookBack - $encodedCharPos);
+ } elseif ($dec < 192) { // Middle byte of a multi byte character, look further back
+ $lookBack += 3;
+ // No encoded character found
+ return $maxLength;
+ * Set the body wrapping.
+ public function setWordWrap()
+ if ($this->WordWrap < 1) {
+ switch ($this->message_type) {
+ case 'alt':
+ case 'alt_inline':
+ case 'alt_attach':
+ case 'alt_inline_attach':
+ $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap);
+ $this->Body = $this->wrapText($this->Body, $this->WordWrap);
+ * Assemble message headers.
+ * @return string The assembled headers
+ public function createHeader()
+ $result = '';
+ // Set the boundaries
+ $uniq_id = md5(uniqid(time()));
+ $this->boundary[1] = 'b1_' . $uniq_id;
+ $this->boundary[2] = 'b2_' . $uniq_id;
+ $this->boundary[3] = 'b3_' . $uniq_id;
+ if ($this->MessageDate == '') {
+ $this->MessageDate = self::rfcDate();
+ $result .= $this->headerLine('Date', $this->MessageDate);
+ // To be created automatically by mail()
+ if ($this->Mailer != 'mail') {
+ $this->SingleToArray[] = $this->addrFormat($toaddr);
+ $result .= $this->addrAppend('To', $this->to);
+ } elseif (count($this->cc) == 0) {
+ $result .= $this->headerLine('To', 'undisclosed-recipients:;');
+ $result .= $this->addrAppend('From', array(array(trim($this->From), $this->FromName)));
+ // sendmail and mail() extract Cc from the header before sending
+ if (count($this->cc) > 0) {
+ $result .= $this->addrAppend('Cc', $this->cc);
+ // sendmail and mail() extract Bcc from the header before sending
+ if ((
+ $this->Mailer == 'sendmail' or $this->Mailer == 'qmail' or $this->Mailer == 'mail'
+ and count($this->bcc) > 0
+ $result .= $this->addrAppend('Bcc', $this->bcc);
+ if (count($this->ReplyTo) > 0) {
+ $result .= $this->addrAppend('Reply-To', $this->ReplyTo);
+ // mail() sets the subject itself
+ $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));
+ if ($this->MessageID != '') {
+ $this->lastMessageID = $this->MessageID;
+ $this->lastMessageID = sprintf('<%s@%s>', $uniq_id, $this->ServerHostname());
+ $result .= $this->HeaderLine('Message-ID', $this->lastMessageID);
+ $result .= $this->headerLine('X-Priority', $this->Priority);
+ if ($this->XMailer == '') {
+ $result .= $this->headerLine(
+ 'X-Mailer',
+ 'PHPMailer ' . $this->Version . ' (https://github.com/PHPMailer/PHPMailer/)'
+ $myXmailer = trim($this->XMailer);
+ if ($myXmailer) {
+ $result .= $this->headerLine('X-Mailer', $myXmailer);
+ if ($this->ConfirmReadingTo != '') {
+ $result .= $this->headerLine('Disposition-Notification-To', '<' . trim($this->ConfirmReadingTo) . '>');
+ // Add custom headers
+ for ($index = 0; $index < count($this->CustomHeader); $index++) {
+ trim($this->CustomHeader[$index][0]),
+ $this->encodeHeader(trim($this->CustomHeader[$index][1]))
+ if (!$this->sign_key_file) {
+ $result .= $this->headerLine('MIME-Version', '1.0');
+ $result .= $this->getMailMIME();
+ * Get the message MIME type headers.
+ public function getMailMIME()
+ $ismultipart = true;
+ case 'inline':
+ $result .= $this->headerLine('Content-Type', 'multipart/related;');
+ $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"');
+ case 'attach':
+ case 'inline_attach':
+ $result .= $this->headerLine('Content-Type', 'multipart/mixed;');
+ $result .= $this->headerLine('Content-Type', 'multipart/alternative;');
+ // Catches case 'plain': and case '':
+ $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
+ $ismultipart = false;
+ // RFC1341 part 5 says 7bit is assumed if not specified
+ if ($this->Encoding != '7bit') {
+ // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE
+ if ($ismultipart) {
+ if ($this->Encoding == '8bit') {
+ $result .= $this->headerLine('Content-Transfer-Encoding', '8bit');
+ // The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible
+ $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding);
+ $result .= $this->LE;
+ * Returns the whole MIME message.
+ * Includes complete headers and body.
+ * Only valid post preSend().
+ * @see PHPMailer::preSend()
+ public function getSentMIMEMessage()
+ return $this->MIMEHeader . $this->mailHeader . self::CRLF . $this->MIMEBody;
+ * Assemble the message body.
+ * Returns an empty string on failure.
+ * @return string The assembled message body
+ public function createBody()
+ $body = '';
+ if ($this->sign_key_file) {
+ $body .= $this->getMailMIME() . $this->LE;
+ $this->setWordWrap();
+ $bodyEncoding = $this->Encoding;
+ $bodyCharSet = $this->CharSet;
+ if ($bodyEncoding == '8bit' and !$this->has8bitChars($this->Body)) {
+ $bodyEncoding = '7bit';
+ $bodyCharSet = 'us-ascii';
+ $altBodyEncoding = $this->Encoding;
+ $altBodyCharSet = $this->CharSet;
+ if ($altBodyEncoding == '8bit' and !$this->has8bitChars($this->AltBody)) {
+ $altBodyEncoding = '7bit';
+ $altBodyCharSet = 'us-ascii';
+ $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
+ $body .= $this->encodeString($this->Body, $bodyEncoding);
+ $body .= $this->LE . $this->LE;
+ $body .= $this->attachAll('inline', $this->boundary[1]);
+ $body .= $this->attachAll('attachment', $this->boundary[1]);
+ $body .= $this->textLine('--' . $this->boundary[1]);
+ $body .= $this->headerLine('Content-Type', 'multipart/related;');
+ $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"');
+ $body .= $this->LE;
+ $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding);
+ $body .= $this->attachAll('inline', $this->boundary[2]);
+ $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, 'text/plain', $altBodyEncoding);
+ $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
+ $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, 'text/html', $bodyEncoding);
+ if (!empty($this->Ical)) {
+ $body .= $this->getBoundary($this->boundary[1], '', 'text/calendar; method=REQUEST', '');
+ $body .= $this->encodeString($this->Ical, $this->Encoding);
+ $body .= $this->endBoundary($this->boundary[1]);
+ $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, 'text/html', $bodyEncoding);
+ $body .= $this->headerLine('Content-Type', 'multipart/alternative;');
+ $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, 'text/plain', $altBodyEncoding);
+ $body .= $this->endBoundary($this->boundary[2]);
+ $body .= $this->textLine('--' . $this->boundary[2]);
+ $body .= $this->textLine("\tboundary=\"" . $this->boundary[3] . '"');
+ $body .= $this->getBoundary($this->boundary[3], $bodyCharSet, 'text/html', $bodyEncoding);
+ $body .= $this->attachAll('inline', $this->boundary[3]);
+ // catch case 'plain' and case ''
+ if ($this->isError()) {
+ } elseif ($this->sign_key_file) {
+ if (!defined('PKCS7_TEXT')) {
+ throw new phpmailerException($this->lang('signing') . ' OpenSSL extension missing.');
+ // @TODO would be nice to use php://temp streams here, but need to wrap for PHP < 5.1
+ $file = tempnam(sys_get_temp_dir(), 'mail');
+ file_put_contents($file, $body); // @TODO check this worked
+ $signed = tempnam(sys_get_temp_dir(), 'signed');
+ if (@openssl_pkcs7_sign(
+ $file,
+ $signed,
+ 'file://' . realpath($this->sign_cert_file),
+ array('file://' . realpath($this->sign_key_file), $this->sign_key_pass),
+ null
+ @unlink($file);
+ $body = file_get_contents($signed);
+ @unlink($signed);
+ throw new phpmailerException($this->lang('signing') . openssl_error_string());
+ return $body;
+ * Return the start of a message boundary.
+ * @param string $boundary
+ * @param string $charSet
+ * @param string $contentType
+ * @param string $encoding
+ protected function getBoundary($boundary, $charSet, $contentType, $encoding)
+ if ($charSet == '') {
+ $charSet = $this->CharSet;
+ if ($contentType == '') {
+ $contentType = $this->ContentType;
+ if ($encoding == '') {
+ $encoding = $this->Encoding;
+ $result .= $this->textLine('--' . $boundary);
+ $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet);
+ if ($encoding != '7bit') {
+ $result .= $this->headerLine('Content-Transfer-Encoding', $encoding);
+ * Return the end of a message boundary.
+ protected function endBoundary($boundary)
+ return $this->LE . '--' . $boundary . '--' . $this->LE;
+ * Set the message type.
+ * PHPMailer only supports some preset message types,
+ * not arbitrary MIME structures.
+ protected function setMessageType()
+ $this->message_type = array();
+ if ($this->alternativeExists()) {
+ $this->message_type[] = 'alt';
+ if ($this->inlineImageExists()) {
+ $this->message_type[] = 'inline';
+ if ($this->attachmentExists()) {
+ $this->message_type[] = 'attach';
+ $this->message_type = implode('_', $this->message_type);
+ if ($this->message_type == '') {
+ $this->message_type = 'plain';
+ * Format a header line.
+ public function headerLine($name, $value)
+ return $name . ': ' . $value . $this->LE;
+ * Return a formatted mail line.
+ public function textLine($value)
+ return $value . $this->LE;
+ * Add an attachment from a path on the filesystem.
+ * Returns false if the file could not be found or read.
+ * @param string $path Path to the attachment.
+ * @param string $name Overrides the attachment name.
+ * @param string $encoding File encoding (see $Encoding).
+ * @param string $type File extension (MIME) type.
+ * @param string $disposition Disposition to use
+ public function addAttachment($path, $name = '', $encoding = 'base64', $type = '', $disposition = 'attachment')
+ if (!@is_file($path)) {
+ throw new phpmailerException($this->lang('file_access') . $path, self::STOP_CONTINUE);
+ // If a MIME type is not specified, try to work it out from the file name
+ if ($type == '') {
+ $type = self::filenameToType($path);
+ $filename = basename($path);
+ if ($name == '') {
+ $name = $filename;
+ $this->attachment[] = array(
+ 0 => $path,
+ 1 => $filename,
+ 2 => $name,
+ 3 => $encoding,
+ 4 => $type,
+ 5 => false, // isStringAttachment
+ 6 => $disposition,
+ 7 => 0
+ * Return the array of attachments.
+ public function getAttachments()
+ return $this->attachment;
+ * Attach all file, string, and binary attachments to the message.
+ * @param string $disposition_type
+ protected function attachAll($disposition_type, $boundary)
+ // Return text of body
+ $mime = array();
+ $cidUniq = array();
+ $incl = array();
+ // Add all attachments
+ foreach ($this->attachment as $attachment) {
+ // Check if it is a valid disposition_filter
+ if ($attachment[6] == $disposition_type) {
+ // Check for string attachment
+ $string = '';
+ $path = '';
+ $bString = $attachment[5];
+ if ($bString) {
+ $string = $attachment[0];
+ $path = $attachment[0];
+ $inclhash = md5(serialize($attachment));
+ if (in_array($inclhash, $incl)) {
+ $incl[] = $inclhash;
+ $name = $attachment[2];
+ $encoding = $attachment[3];
+ $type = $attachment[4];
+ $disposition = $attachment[6];
+ $cid = $attachment[7];
+ if ($disposition == 'inline' && isset($cidUniq[$cid])) {
+ $cidUniq[$cid] = true;
+ $mime[] = sprintf('--%s%s', $boundary, $this->LE);
+ $mime[] = sprintf(
+ 'Content-Type: %s; name="%s"%s',
+ $type,
+ $this->encodeHeader($this->secureHeader($name)),
+ $this->LE
+ $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, $this->LE);
+ if ($disposition == 'inline') {
+ $mime[] = sprintf('Content-ID: <%s>%s', $cid, $this->LE);
+ // If a filename contains any of these chars, it should be quoted,
+ // but not otherwise: RFC2183 & RFC2045 5.1
+ // Fixes a warning in IETF's msglint MIME checker
+ // Allow for bypassing the Content-Disposition header totally
+ if (!(empty($disposition))) {
+ if (preg_match('/[ \(\)<>@,;:\\"\/\[\]\?=]/', $name)) {
+ 'Content-Disposition: %s; filename="%s"%s',
+ $disposition,
+ $this->LE . $this->LE
+ 'Content-Disposition: %s; filename=%s%s',
+ $mime[] = $this->LE;
+ // Encode as string attachment
+ $mime[] = $this->encodeString($string, $encoding);
+ $mime[] = $this->LE . $this->LE;
+ $mime[] = $this->encodeFile($path, $encoding);
+ $mime[] = sprintf('--%s--%s', $boundary, $this->LE);
+ return implode('', $mime);
+ * Encode a file attachment in requested format.
+ * @param string $path The full path to the file
+ * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
+ * @see EncodeFile(encodeFile
+ protected function encodeFile($path, $encoding = 'base64')
+ if (!is_readable($path)) {
+ throw new phpmailerException($this->lang('file_open') . $path, self::STOP_CONTINUE);
+ $magic_quotes = get_magic_quotes_runtime();
+ if ($magic_quotes) {
+ if (version_compare(PHP_VERSION, '5.3.0', '<')) {
+ set_magic_quotes_runtime(false);
+ //Doesn't exist in PHP 5.4, but we don't need to check because
+ //get_magic_quotes_runtime always returns false in 5.4+
+ //so it will never get here
+ ini_set('magic_quotes_runtime', 0);
+ $file_buffer = file_get_contents($path);
+ $file_buffer = $this->encodeString($file_buffer, $encoding);
+ set_magic_quotes_runtime($magic_quotes);
+ ini_set('magic_quotes_runtime', ($magic_quotes?'1':'0'));
+ return $file_buffer;
+ } catch (Exception $exc) {
+ * Encode a string in requested format.
+ * @param string $str The text to encode
+ public function encodeString($str, $encoding = 'base64')
+ $encoded = '';
+ switch (strtolower($encoding)) {
+ case 'base64':
+ $encoded = chunk_split(base64_encode($str), 76, $this->LE);
+ case '7bit':
+ case '8bit':
+ $encoded = $this->fixEOL($str);
+ // Make sure it ends with a line break
+ if (substr($encoded, -(strlen($this->LE))) != $this->LE) {
+ $encoded .= $this->LE;
+ case 'binary':
+ $encoded = $str;
+ case 'quoted-printable':
+ $encoded = $this->encodeQP($str);
+ $this->setError($this->lang('encoding') . $encoding);
+ return $encoded;
+ * Encode a header string optimally.
+ * Picks shortest of Q, B, quoted-printable or none.
+ * @param string $position
+ public function encodeHeader($str, $position = 'text')
+ $matchcount = 0;
+ switch (strtolower($position)) {
+ case 'phrase':
+ if (!preg_match('/[\200-\377]/', $str)) {
+ // Can't use addslashes as we don't know the value of magic_quotes_sybase
+ $encoded = addcslashes($str, "\0..\37\177\\\"");
+ if (($str == $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
+ return ($encoded);
+ return ("\"$encoded\"");
+ $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches);
+ /** @noinspection PhpMissingBreakStatementInspection */
+ case 'comment':
+ $matchcount = preg_match_all('/[()"]/', $str, $matches);
+ // Intentional fall-through
+ case 'text':
+ $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches);
+ if ($matchcount == 0) { // There are no chars that need encoding
+ return ($str);
+ $maxlen = 75 - 7 - strlen($this->CharSet);
+ // Try to select the encoding which should produce the shortest output
+ if ($matchcount > strlen($str) / 3) {
+ // More than a third of the content will need encoding, so B encoding will be most efficient
+ $encoding = 'B';
+ if (function_exists('mb_strlen') && $this->hasMultiBytes($str)) {
+ // Use a custom function which correctly encodes and wraps long
+ // multibyte strings without breaking lines within a character
+ $encoded = $this->base64EncodeWrapMB($str, "\n");
+ $encoded = base64_encode($str);
+ $maxlen -= $maxlen % 4;
+ $encoded = trim(chunk_split($encoded, $maxlen, "\n"));
+ $encoding = 'Q';
+ $encoded = $this->encodeQ($str, $position);
+ $encoded = $this->wrapText($encoded, $maxlen, true);
+ $encoded = str_replace('=' . self::CRLF, "\n", trim($encoded));
+ $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded);
+ $encoded = trim(str_replace("\n", $this->LE, $encoded));
+ * Check if a string contains multi-byte characters.
+ * @param string $str multi-byte text to wrap encode
+ public function hasMultiBytes($str)
+ if (function_exists('mb_strlen')) {
+ return (strlen($str) > mb_strlen($str, $this->CharSet));
+ } else { // Assume no multibytes (we can't handle without mbstring functions anyway)
+ * Does a string contain any 8-bit chars (in any charset)?
+ * @param string $text
+ public function has8bitChars($text)
+ return (boolean)preg_match('/[\x80-\xFF]/', $text);
+ * Encode and wrap long multibyte strings for mail headers
+ * without breaking lines within a character.
+ * Adapted from a function by paravoid
+ * @link http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283
+ * @param string $linebreak string to use as linefeed/end-of-line
+ public function base64EncodeWrapMB($str, $linebreak = null)
+ $start = '=?' . $this->CharSet . '?B?';
+ $end = '?=';
+ if ($linebreak === null) {
+ $linebreak = $this->LE;
+ $mb_length = mb_strlen($str, $this->CharSet);
+ // Each line must have length <= 75, including $start and $end
+ $length = 75 - strlen($start) - strlen($end);
+ // Average multi-byte ratio
+ $ratio = $mb_length / strlen($str);
+ // Base64 has a 4:3 ratio
+ $avgLength = floor($length * $ratio * .75);
+ for ($i = 0; $i < $mb_length; $i += $offset) {
+ $lookBack = 0;
+ do {
+ $offset = $avgLength - $lookBack;
+ $chunk = mb_substr($str, $i, $offset, $this->CharSet);
+ $chunk = base64_encode($chunk);
+ $lookBack++;
+ } while (strlen($chunk) > $length);
+ $encoded .= $chunk . $linebreak;
+ // Chomp the last linefeed
+ $encoded = substr($encoded, 0, -strlen($linebreak));
+ * Encode a string in quoted-printable format.
+ * According to RFC2045 section 6.7.
+ * @param string $string The text to encode
+ * @param integer $line_max Number of chars allowed on a line before wrapping
+ * @link http://www.php.net/manual/en/function.quoted-printable-decode.php#89417 Adapted from this comment
+ public function encodeQP($string, $line_max = 76)
+ if (function_exists('quoted_printable_encode')) { // Use native function if it's available (>= PHP5.3)
+ return $this->fixEOL(quoted_printable_encode($string));
+ // Fall back to a pure PHP implementation
+ $string = str_replace(
+ array('%20', '%0D%0A.', '%0D%0A', '%'),
+ array(' ', "\r\n=2E", "\r\n", '='),
+ rawurlencode($string)
+ $string = preg_replace('/[^\r\n]{' . ($line_max - 3) . '}[^=\r\n]{2}/', "$0=\r\n", $string);
+ return $this->fixEOL($string);
+ * Backward compatibility wrapper for an old QP encoding function that was removed.
+ * @see PHPMailer::encodeQP()
+ * @param integer $line_max
+ * @param boolean $space_conv
+ * @deprecated Use encodeQP instead.
+ public function encodeQPphp(
+ $string,
+ $line_max = 76,
+ /** @noinspection PhpUnusedParameterInspection */ $space_conv = false
+ return $this->encodeQP($string, $line_max);
+ * Encode a string using Q encoding.
+ * @link http://tools.ietf.org/html/rfc2047
+ * @param string $str the text to encode
+ * @param string $position Where the text is going to be used, see the RFC for what that means
+ public function encodeQ($str, $position = 'text')
+ // There should not be any EOL in the string
+ $pattern = '';
+ $encoded = str_replace(array("\r", "\n"), '', $str);
+ // RFC 2047 section 5.3
+ $pattern = '^A-Za-z0-9!*+\/ -';
+ // RFC 2047 section 5.2
+ $pattern = '\(\)"';
+ // intentional fall-through
+ // for this reason we build the $pattern without including delimiters and []
+ // RFC 2047 section 5.1
+ // Replace every high ascii, control, =, ? and _ characters
+ $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern;
+ $matches = array();
+ if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) {
+ // If the string contains an '=', make sure it's the first thing we replace
+ // so as to avoid double-encoding
+ $eqkey = array_search('=', $matches[0]);
+ if ($eqkey !== false) {
+ unset($matches[0][$eqkey]);
+ array_unshift($matches[0], '=');
+ foreach (array_unique($matches[0]) as $char) {
+ $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded);
+ // Replace every spaces to _ (more readable than =20)
+ return str_replace(' ', '_', $encoded);
+ * Add a string or binary attachment (non-filesystem).
+ * This method can be used to attach ascii or binary data,
+ * such as a BLOB record from a database.
+ * @param string $string String attachment data.
+ * @param string $filename Name of the attachment.
+ public function addStringAttachment(
+ $filename,
+ $encoding = 'base64',
+ $type = '',
+ $disposition = 'attachment'
+ $type = self::filenameToType($filename);
+ // Append to $attachment array
+ 0 => $string,
+ 2 => basename($filename),
+ 5 => true, // isStringAttachment
+ * Add an embedded (inline) attachment from a file.
+ * This can include images, sounds, and just about any other document type.
+ * These differ from 'regular' attachmants in that they are intended to be
+ * displayed inline with the message, not just attached for download.
+ * This is used in HTML messages that embed the images
+ * the HTML refers to using the $cid value.
+ * @param string $cid Content ID of the attachment; Use this to reference
+ * the content when using an embedded image in HTML.
+ * @param string $type File MIME type.
+ * @return boolean True on successfully adding an attachment
+ public function addEmbeddedImage($path, $cid, $name = '', $encoding = 'base64', $type = '', $disposition = 'inline')
+ $this->setError($this->lang('file_access') . $path);
+ 7 => $cid
+ * Add an embedded stringified attachment.
+ * Be sure to set the $type to an image type for images:
+ * JPEG images use 'image/jpeg', GIF uses 'image/gif', PNG uses 'image/png'.
+ * @param string $string The attachment binary data.
+ * @param string $type MIME type.
+ public function addStringEmbeddedImage(
+ $cid,
+ $name = '',
+ $disposition = 'inline'
+ // If a MIME type is not specified, try to work it out from the name
+ $type = self::filenameToType($name);
+ 1 => $name,
+ * Check if an inline attachment is present.
+ public function inlineImageExists()
+ if ($attachment[6] == 'inline') {
+ * Check if an attachment (non-inline) is present.
+ public function attachmentExists()
+ if ($attachment[6] == 'attachment') {
+ * Check if this message has an alternative body set.
+ public function alternativeExists()
+ return !empty($this->AltBody);
+ * Clear all To recipients.
+ public function clearAddresses()
+ unset($this->all_recipients[strtolower($to[0])]);
+ $this->to = array();
+ * Clear all CC recipients.
+ public function clearCCs()
+ unset($this->all_recipients[strtolower($cc[0])]);
+ $this->cc = array();
+ * Clear all BCC recipients.
+ public function clearBCCs()
+ unset($this->all_recipients[strtolower($bcc[0])]);
+ $this->bcc = array();
+ * Clear all ReplyTo recipients.
+ public function clearReplyTos()
+ $this->ReplyTo = array();
+ * Clear all recipient types.
+ public function clearAllRecipients()
+ $this->all_recipients = array();
+ * Clear all filesystem, string, and binary attachments.
+ public function clearAttachments()
+ $this->attachment = array();
+ * Clear all custom headers.
+ public function clearCustomHeaders()
+ $this->CustomHeader = array();
+ * Add an error message to the error container.
+ * @param string $msg
+ protected function setError($msg)
+ $this->error_count++;
+ if ($this->Mailer == 'smtp' and !is_null($this->smtp)) {
+ $lasterror = $this->smtp->getError();
+ if (!empty($lasterror) and array_key_exists('smtp_msg', $lasterror)) {
+ $msg .= '<p>' . $this->lang('smtp_error') . $lasterror['smtp_msg'] . "</p>\n";
+ $this->ErrorInfo = $msg;
+ * Return an RFC 822 formatted date.
+ public static function rfcDate()
+ // Set the time zone to whatever the default is to avoid 500 errors
+ // Will default to UTC if it's not set properly in php.ini
+ date_default_timezone_set(@date_default_timezone_get());
+ return date('D, j M Y H:i:s O');
+ * Get the server hostname.
+ * Returns 'localhost.localdomain' if unknown.
+ protected function serverHostname()
+ $result = 'localhost.localdomain';
+ if (!empty($this->Hostname)) {
+ $result = $this->Hostname;
+ } elseif (isset($_SERVER) and array_key_exists('SERVER_NAME', $_SERVER) and !empty($_SERVER['SERVER_NAME'])) {
+ $result = $_SERVER['SERVER_NAME'];
+ } elseif (function_exists('gethostname') && gethostname() !== false) {
+ $result = gethostname();
+ } elseif (php_uname('n') !== false) {
+ $result = php_uname('n');
+ * Get an error message in the current language.
+ protected function lang($key)
+ if (count($this->language) < 1) {
+ $this->setLanguage('en'); // set the default language
+ if (isset($this->language[$key])) {
+ return $this->language[$key];
+ return 'Language string failed to load: ' . $key;
+ * Check if an error occurred.
+ * @return boolean True if an error did occur.
+ public function isError()
+ return ($this->error_count > 0);
+ * Ensure consistent line endings in a string.
+ * Changes every end of line from CRLF, CR or LF to $this->LE.
+ * @param string $str String to fixEOL
+ public function fixEOL($str)
+ // Normalise to \n
+ $nstr = str_replace(array("\r\n", "\r"), "\n", $str);
+ // Now convert LE as needed
+ if ($this->LE !== "\n") {
+ $nstr = str_replace("\n", $this->LE, $nstr);
+ return $nstr;
+ * Add a custom header.
+ * $name value can be overloaded to contain
+ * both header name and value (name:value)
+ * @param string $name Custom header name
+ * @param string $value Header value
+ public function addCustomHeader($name, $value = null)
+ if ($value === null) {
+ // Value passed in as name:value
+ $this->CustomHeader[] = explode(':', $name, 2);
+ $this->CustomHeader[] = array($name, $value);
+ * Create a message from an HTML string.
+ * Automatically makes modifications for inline images and backgrounds
+ * and creates a plain-text version by converting the HTML.
+ * Overwrites any existing values in $this->Body and $this->AltBody
+ * @param string $message HTML message string
+ * @param string $basedir baseline directory for path
+ * @param boolean $advanced Whether to use the advanced HTML to text converter
+ * @return string $message
+ public function msgHTML($message, $basedir = '', $advanced = false)
+ preg_match_all('/(src|background)=["\'](.*)["\']/Ui', $message, $images);
+ if (isset($images[2])) {
+ foreach ($images[2] as $imgindex => $url) {
+ // do not change urls for absolute images (thanks to corvuscorax)
+ if (!preg_match('#^[A-z]+://#', $url)) {
+ $filename = basename($url);
+ $directory = dirname($url);
+ if ($directory == '.') {
+ $directory = '';
+ $cid = md5($url) . '@phpmailer.0'; // RFC2392 S 2
+ if (strlen($basedir) > 1 && substr($basedir, -1) != '/') {
+ $basedir .= '/';
+ if (strlen($directory) > 1 && substr($directory, -1) != '/') {
+ $directory .= '/';
+ if ($this->addEmbeddedImage(
+ $basedir . $directory . $filename,
+ 'base64',
+ self::_mime_types(self::mb_pathinfo($filename, PATHINFO_EXTENSION))
+ $message = preg_replace(
+ '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui',
+ $images[1][$imgindex] . '="cid:' . $cid . '"',
+ $message
+ $this->isHTML(true);
+ // Convert all message body line breaks to CRLF, makes quoted-printable encoding work much better
+ $this->Body = $this->normalizeBreaks($message);
+ $this->AltBody = $this->normalizeBreaks($this->html2text($message, $advanced));
+ if (empty($this->AltBody)) {
+ $this->AltBody = 'To view this email message, open it in a program that understands HTML!' .
+ self::CRLF . self::CRLF;
+ return $this->Body;
+ * Convert an HTML string into plain text.
+ * @param string $html The HTML text to convert
+ * @param boolean $advanced Should this use the more complex html2text converter or just a simple one?
+ public function html2text($html, $advanced = false)
+ if ($advanced) {
+ require_once 'extras/class.html2text.php';
+ $htmlconverter = new html2text($html);
+ return $htmlconverter->get_text();
+ return html_entity_decode(
+ trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
+ ENT_QUOTES,
+ $this->CharSet
+ * Get the MIME type for a file extension.
+ * @param string $ext File extension
+ * @return string MIME type of file.
+ public static function _mime_types($ext = '')
+ $mimes = array(
+ 'xl' => 'application/excel',
+ 'hqx' => 'application/mac-binhex40',
+ 'cpt' => 'application/mac-compactpro',
+ 'bin' => 'application/macbinary',
+ 'doc' => 'application/msword',
+ 'word' => 'application/msword',
+ 'class' => 'application/octet-stream',
+ 'dll' => 'application/octet-stream',
+ 'dms' => 'application/octet-stream',
+ 'exe' => 'application/octet-stream',
+ 'lha' => 'application/octet-stream',
+ 'lzh' => 'application/octet-stream',
+ 'psd' => 'application/octet-stream',
+ 'sea' => 'application/octet-stream',
+ 'so' => 'application/octet-stream',
+ 'oda' => 'application/oda',
+ 'pdf' => 'application/pdf',
+ 'ai' => 'application/postscript',
+ 'eps' => 'application/postscript',
+ 'ps' => 'application/postscript',
+ 'smi' => 'application/smil',
+ 'smil' => 'application/smil',
+ 'mif' => 'application/vnd.mif',
+ 'xls' => 'application/vnd.ms-excel',
+ 'ppt' => 'application/vnd.ms-powerpoint',
+ 'wbxml' => 'application/vnd.wap.wbxml',
+ 'wmlc' => 'application/vnd.wap.wmlc',
+ 'dcr' => 'application/x-director',
+ 'dir' => 'application/x-director',
+ 'dxr' => 'application/x-director',
+ 'dvi' => 'application/x-dvi',
+ 'gtar' => 'application/x-gtar',
+ 'php3' => 'application/x-httpd-php',
+ 'php4' => 'application/x-httpd-php',
+ 'php' => 'application/x-httpd-php',
+ 'phtml' => 'application/x-httpd-php',
+ 'phps' => 'application/x-httpd-php-source',
+ 'js' => 'application/x-javascript',
+ 'swf' => 'application/x-shockwave-flash',
+ 'sit' => 'application/x-stuffit',
+ 'tar' => 'application/x-tar',
+ 'tgz' => 'application/x-tar',
+ 'xht' => 'application/xhtml+xml',
+ 'xhtml' => 'application/xhtml+xml',
+ 'zip' => 'application/zip',
+ 'mid' => 'audio/midi',
+ 'midi' => 'audio/midi',
+ 'mp2' => 'audio/mpeg',
+ 'mp3' => 'audio/mpeg',
+ 'mpga' => 'audio/mpeg',
+ 'aif' => 'audio/x-aiff',
+ 'aifc' => 'audio/x-aiff',
+ 'aiff' => 'audio/x-aiff',
+ 'ram' => 'audio/x-pn-realaudio',
+ 'rm' => 'audio/x-pn-realaudio',
+ 'rpm' => 'audio/x-pn-realaudio-plugin',
+ 'ra' => 'audio/x-realaudio',
+ 'wav' => 'audio/x-wav',
+ 'bmp' => 'image/bmp',
+ 'gif' => 'image/gif',
+ 'jpeg' => 'image/jpeg',
+ 'jpe' => 'image/jpeg',
+ 'jpg' => 'image/jpeg',
+ 'png' => 'image/png',
+ 'tiff' => 'image/tiff',
+ 'tif' => 'image/tiff',
+ 'eml' => 'message/rfc822',
+ 'css' => 'text/css',
+ 'html' => 'text/html',
+ 'htm' => 'text/html',
+ 'shtml' => 'text/html',
+ 'log' => 'text/plain',
+ 'text' => 'text/plain',
+ 'txt' => 'text/plain',
+ 'rtx' => 'text/richtext',
+ 'rtf' => 'text/rtf',
+ 'vcf' => 'text/vcard',
+ 'vcard' => 'text/vcard',
+ 'xml' => 'text/xml',
+ 'xsl' => 'text/xml',
+ 'mpeg' => 'video/mpeg',
+ 'mpe' => 'video/mpeg',
+ 'mpg' => 'video/mpeg',
+ 'mov' => 'video/quicktime',
+ 'qt' => 'video/quicktime',
+ 'rv' => 'video/vnd.rn-realvideo',
+ 'avi' => 'video/x-msvideo',
+ 'movie' => 'video/x-sgi-movie'
+ return (array_key_exists(strtolower($ext), $mimes) ? $mimes[strtolower($ext)]: 'application/octet-stream');
+ * Map a file name to a MIME type.
+ * Defaults to 'application/octet-stream', i.e.. arbitrary binary data.
+ * @param string $filename A file name or full path, does not need to exist as a file
+ public static function filenameToType($filename)
+ // In case the path is a URL, strip any query string before getting extension
+ $qpos = strpos($filename, '?');
+ if ($qpos !== false) {
+ $filename = substr($filename, 0, $qpos);
+ $pathinfo = self::mb_pathinfo($filename);
+ return self::_mime_types($pathinfo['extension']);
+ * Multi-byte-safe pathinfo replacement.
+ * Drop-in replacement for pathinfo(), but multibyte-safe, cross-platform-safe, old-version-safe.
+ * Works similarly to the one in PHP >= 5.2.0
+ * @link http://www.php.net/manual/en/function.pathinfo.php#107461
+ * @param string $path A filename or path, does not need to exist as a file
+ * @param integer|string $options Either a PATHINFO_* constant,
+ * or a string name to return only the specified piece, allows 'filename' to work on PHP < 5.2
+ * @return string|array
+ public static function mb_pathinfo($path, $options = null)
+ $ret = array('dirname' => '', 'basename' => '', 'extension' => '', 'filename' => '');
+ $pathinfo = array();
+ if (preg_match('%^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$%im', $path, $pathinfo)) {
+ if (array_key_exists(1, $pathinfo)) {
+ $ret['dirname'] = $pathinfo[1];
+ if (array_key_exists(2, $pathinfo)) {
+ $ret['basename'] = $pathinfo[2];
+ if (array_key_exists(5, $pathinfo)) {
+ $ret['extension'] = $pathinfo[5];
+ if (array_key_exists(3, $pathinfo)) {
+ $ret['filename'] = $pathinfo[3];
+ switch ($options) {
+ case PATHINFO_DIRNAME:
+ case 'dirname':
+ return $ret['dirname'];
+ case PATHINFO_BASENAME:
+ case 'basename':
+ return $ret['basename'];
+ case PATHINFO_EXTENSION:
+ case 'extension':
+ return $ret['extension'];
+ case PATHINFO_FILENAME:
+ case 'filename':
+ return $ret['filename'];
+ return $ret;
+ * Set or reset instance properties.
+ * Usage Example:
+ * $page->set('X-Priority', '3');
+ * NOTE: will not work with arrays, there are no arrays to set/reset
+ * @TODO Should this not be using __set() magic function?
+ public function set($name, $value = '')
+ if (isset($this->$name)) {
+ $this->$name = $value;
+ throw new phpmailerException($this->lang('variable_set') . $name, self::STOP_CRITICAL);
+ if ($exc->getCode() == self::STOP_CRITICAL) {
+ * Strip newlines to prevent header injection.
+ public function secureHeader($str)
+ return trim(str_replace(array("\r", "\n"), '', $str));
+ * Normalize line breaks in a string.
+ * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format.
+ * Defaults to CRLF (for message bodies) and preserves consecutive breaks.
+ * @param string $breaktype What kind of line break to use, defaults to CRLF
+ public static function normalizeBreaks($text, $breaktype = "\r\n")
+ return preg_replace('/(\r\n|\r|\n)/ms', $breaktype, $text);
+ * Set the public and private key files and password for S/MIME signing.
+ * @param string $cert_filename
+ * @param string $key_filename
+ * @param string $key_pass Password for private key
+ public function sign($cert_filename, $key_filename, $key_pass)
+ $this->sign_cert_file = $cert_filename;
+ $this->sign_key_file = $key_filename;
+ $this->sign_key_pass = $key_pass;
+ * Quoted-Printable-encode a DKIM header.
+ * @param string $txt
+ public function DKIM_QP($txt)
+ $line = '';
+ for ($i = 0; $i < strlen($txt); $i++) {
+ $ord = ord($txt[$i]);
+ if (((0x21 <= $ord) && ($ord <= 0x3A)) || $ord == 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E))) {
+ $line .= $txt[$i];
+ $line .= '=' . sprintf('%02X', $ord);
+ return $line;
+ * Generate a DKIM signature.
+ * @param string $signHeader
+ public function DKIM_Sign($signHeader)
+ $privKeyStr = file_get_contents($this->DKIM_private);
+ if ($this->DKIM_passphrase != '') {
+ $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase);
+ $privKey = $privKeyStr;
+ if (openssl_sign($signHeader, $signature, $privKey)) {
+ return base64_encode($signature);
+ * Generate a DKIM canonicalization header.
+ * @param string $signHeader Header
+ public function DKIM_HeaderC($signHeader)
+ $signHeader = preg_replace('/\r\n\s+/', ' ', $signHeader);
+ $lines = explode("\r\n", $signHeader);
+ foreach ($lines as $key => $line) {
+ list($heading, $value) = explode(':', $line, 2);
+ $heading = strtolower($heading);
+ $value = preg_replace('/\s+/', ' ', $value); // Compress useless spaces
+ $lines[$key] = $heading . ':' . trim($value); // Don't forget to remove WSP around the value
+ $signHeader = implode("\r\n", $lines);
+ return $signHeader;
+ * Generate a DKIM canonicalization body.
+ public function DKIM_BodyC($body)
+ if ($body == '') {
+ return "\r\n";
+ // stabilize line endings
+ $body = str_replace("\r\n", "\n", $body);
+ $body = str_replace("\n", "\r\n", $body);
+ // END stabilize line endings
+ while (substr($body, strlen($body) - 4, 4) == "\r\n\r\n") {
+ $body = substr($body, 0, strlen($body) - 2);
+ * Create the DKIM header and body in a new message header.
+ * @param string $headers_line Header lines
+ * @param string $body Body
+ public function DKIM_Add($headers_line, $subject, $body)
+ $DKIMsignatureType = 'rsa-sha1'; // Signature & hash algorithms
+ $DKIMcanonicalization = 'relaxed/simple'; // Canonicalization of header/body
+ $DKIMquery = 'dns/txt'; // Query method
+ $DKIMtime = time(); // Signature Timestamp = seconds since 00:00:00 - Jan 1, 1970 (UTC time zone)
+ $subject_header = "Subject: $subject";
+ $headers = explode($this->LE, $headers_line);
+ $from_header = '';
+ $to_header = '';
+ $current = '';
+ foreach ($headers as $header) {
+ if (strpos($header, 'From:') === 0) {
+ $from_header = $header;
+ $current = 'from_header';
+ } elseif (strpos($header, 'To:') === 0) {
+ $to_header = $header;
+ $current = 'to_header';
+ if ($current && strpos($header, ' =?') === 0) {
+ $current .= $header;
+ $from = str_replace('|', '=7C', $this->DKIM_QP($from_header));
+ $to = str_replace('|', '=7C', $this->DKIM_QP($to_header));
+ $subject = str_replace(
+ '|',
+ '=7C',
+ $this->DKIM_QP($subject_header)
+ ); // Copied header fields (dkim-quoted-printable)
+ $body = $this->DKIM_BodyC($body);
+ $DKIMlen = strlen($body); // Length of body
+ $DKIMb64 = base64_encode(pack('H*', sha1($body))); // Base64 of packed binary SHA-1 hash of body
+ $ident = ($this->DKIM_identity == '') ? '' : ' i=' . $this->DKIM_identity . ';';
+ $dkimhdrs = 'DKIM-Signature: v=1; a=' .
+ $DKIMsignatureType . '; q=' .
+ $DKIMquery . '; l=' .
+ $DKIMlen . '; s=' .
+ $this->DKIM_selector .
+ ";\r\n" .
+ "\tt=" . $DKIMtime . '; c=' . $DKIMcanonicalization . ";\r\n" .
+ "\th=From:To:Subject;\r\n" .
+ "\td=" . $this->DKIM_domain . ';' . $ident . "\r\n" .
+ "\tz=$from\r\n" .
+ "\t|$to\r\n" .
+ "\t|$subject;\r\n" .
+ "\tbh=" . $DKIMb64 . ";\r\n" .
+ "\tb=";
+ $toSign = $this->DKIM_HeaderC(
+ $from_header . "\r\n" . $to_header . "\r\n" . $subject_header . "\r\n" . $dkimhdrs
+ $signed = $this->DKIM_Sign($toSign);
+ return $dkimhdrs . $signed . "\r\n";
+ * Allows for public read access to 'to' property.
+ public function getToAddresses()
+ return $this->to;
+ * Allows for public read access to 'cc' property.
+ public function getCcAddresses()
+ return $this->cc;
+ * Allows for public read access to 'bcc' property.
+ public function getBccAddresses()
+ return $this->bcc;
+ * Allows for public read access to 'ReplyTo' property.
+ public function getReplyToAddresses()
+ return $this->ReplyTo;
+ * Allows for public read access to 'all_recipients' property.
+ public function getAllRecipientAddresses()
+ return $this->all_recipients;
+ * Perform a callback.
+ * @param boolean $isSent
+ * @param array $to
+ * @param array $cc
+ * @param array $bcc
+ * @param string $body
+ * @param string $from
+ protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from)
+ if (!empty($this->action_function) && is_callable($this->action_function)) {
+ $params = array($isSent, $to, $cc, $bcc, $subject, $body, $from);
+ call_user_func_array($this->action_function, $params);
+ * PHPMailer exception handler
+class phpmailerException extends \Exception
+ * Prettify error message output
+ public function errorMessage()
+ $errorMsg = '<strong>' . $this->getMessage() . "</strong><br />\n";
+ return $errorMsg;
@@ -0,0 +1,397 @@
+ * PHPMailer POP-Before-SMTP Authentication Class.
+ * @link https://github.com/PHPMailer/PHPMailer/
+ * Specifically for PHPMailer to use for RFC1939 POP-before-SMTP authentication.
+ * Does not support APOP.
+ * @author Richard Davey (original author) <[email protected]>
+class POP3
+ * The POP3 PHPMailer Version number.
+ * Default POP3 port number.
+ public $POP3_PORT = 110;
+ * Default timeout in seconds.
+ public $POP3_TIMEOUT = 30;
+ * POP3 Carriage Return + Line Feed.
+ * @deprecated Use the constant instead
+ public $CRLF = "\r\n";
+ * Debug display level.
+ * Options: 0 = no, 1+ = yes
+ public $do_debug = 0;
+ * POP3 mail server hostname.
+ public $host;
+ * POP3 port number.
+ public $port;
+ * POP3 Timeout Value in seconds.
+ public $tval;
+ * POP3 username
+ public $username;
+ * POP3 password.
+ public $password;
+ * Resource handle for the POP3 connection socket.
+ * @type resource
+ private $pop_conn;
+ * Are we connected?
+ private $connected = false;
+ * Error container.
+ private $errors = array();
+ * Line break constant
+ * Simple static wrapper for all-in-one POP before SMTP
+ * @param $host
+ * @param boolean $port
+ * @param boolean $tval
+ * @param string $username
+ * @param integer $debug_level
+ public static function popBeforeSmtp(
+ $host,
+ $port = false,
+ $tval = false,
+ $username = '',
+ $password = '',
+ $debug_level = 0
+ $pop = new POP3;
+ return $pop->authorise($host, $port, $tval, $username, $password, $debug_level);
+ * Authenticate with a POP3 server.
+ * A connect, login, disconnect sequence
+ * appropriate for POP-before SMTP authorisation.
+ * @param string $host
+ * @param integer|boolean $port
+ * @param integer|boolean $tval
+ public function authorise($host, $port = false, $tval = false, $username = '', $password = '', $debug_level = 0)
+ $this->host = $host;
+ // If no port value provided, use default
+ if ($port === false) {
+ $this->port = $this->POP3_PORT;
+ $this->port = $port;
+ // If no timeout value provided, use default
+ if ($tval === false) {
+ $this->tval = $this->POP3_TIMEOUT;
+ $this->tval = $tval;
+ $this->do_debug = $debug_level;
+ $this->username = $username;
+ $this->password = $password;
+ // Reset the error log
+ $this->errors = array();
+ // connect
+ $result = $this->connect($this->host, $this->port, $this->tval);
+ if ($result) {
+ $login_result = $this->login($this->username, $this->password);
+ if ($login_result) {
+ $this->disconnect();
+ // We need to disconnect regardless of whether the login succeeded
+ * Connect to a POP3 server.
+ * @param integer $tval
+ public function connect($host, $port = false, $tval = 30)
+ // Are we already connected?
+ if ($this->connected) {
+ //On Windows this will raise a PHP Warning error if the hostname doesn't exist.
+ //Rather than suppress it with @fsockopen, capture it cleanly instead
+ set_error_handler(array($this, 'catchWarning'));
+ $port = $this->POP3_PORT;
+ // connect to the POP3 server
+ $this->pop_conn = fsockopen(
+ $host, // POP3 Host
+ $port, // Port #
+ $errno, // Error Number
+ $errstr, // Error Message
+ $tval
+ ); // Timeout (seconds)
+ // Restore the error handler
+ restore_error_handler();
+ // Did we connect?
+ if ($this->pop_conn === false) {
+ // It would appear not...
+ $this->setError(array(
+ 'error' => "Failed to connect to server $host on port $port",
+ 'errno' => $errno,
+ 'errstr' => $errstr
+ // Increase the stream time-out
+ stream_set_timeout($this->pop_conn, $tval, 0);
+ // Get the POP3 server response
+ $pop3_response = $this->getResponse();
+ // Check for the +OK
+ if ($this->checkResponse($pop3_response)) {
+ // The connection is established and the POP3 server is talking
+ $this->connected = true;
+ * Log in to the POP3 server.
+ * Does not support APOP (RFC 2828, 4949).
+ public function login($username = '', $password = '')
+ if (!$this->connected) {
+ $this->setError('Not connected to POP3 server');
+ if (empty($username)) {
+ $username = $this->username;
+ if (empty($password)) {
+ $password = $this->password;
+ // Send the Username
+ $this->sendString("USER $username" . self::CRLF);
+ // Send the Password
+ $this->sendString("PASS $password" . self::CRLF);
+ * Disconnect from the POP3 server.
+ public function disconnect()
+ $this->sendString('QUIT');
+ //The QUIT command may cause the daemon to exit, which will kill our connection
+ //So ignore errors here
+ @fclose($this->pop_conn);
+ } catch (Exception $e) {
+ //Do nothing
+ * Get a response from the POP3 server.
+ * $size is the maximum number of bytes to retrieve
+ * @param integer $size
+ private function getResponse($size = 128)
+ $response = fgets($this->pop_conn, $size);
+ if ($this->do_debug >= 1) {
+ echo "Server -> Client: $response";
+ return $response;
+ * Send raw data to the POP3 server.
+ private function sendString($string)
+ if ($this->pop_conn) {
+ if ($this->do_debug >= 2) { //Show client messages when debug >= 2
+ echo "Client -> Server: $string";
+ return fwrite($this->pop_conn, $string, strlen($string));
+ * Checks the POP3 server response.
+ * Looks for for +OK or -ERR.
+ private function checkResponse($string)
+ if (substr($string, 0, 3) !== '+OK') {
+ 'error' => "Server reported an error: $string",
+ 'errno' => 0,
+ 'errstr' => ''
+ * Add an error to the internal error store.
+ * Also display debug output if it's enabled.
+ * @param $error
+ private function setError($error)
+ $this->errors[] = $error;
+ echo '<pre>';
+ foreach ($this->errors as $error) {
+ print_r($error);
+ echo '</pre>';
+ * POP3 connection error handler.
+ * @param integer $errno
+ * @param string $errstr
+ * @param string $errfile
+ * @param integer $errline
+ private function catchWarning($errno, $errstr, $errfile, $errline)
+ 'error' => "Connecting to the POP3 server raised a PHP warning: ",
+ 'errstr' => $errstr,
+ 'errfile' => $errfile,
+ 'errline' => $errline
@@ -0,0 +1,941 @@
+ * PHPMailer RFC821 SMTP email transport class.
+ * @copyright 2014 Marcus Bointon
+ * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
+ * @author Chris Ryan <[email protected]>
+ * @author Marcus Bointon <[email protected]>
+class SMTP
+ * The PHPMailer SMTP version number.
+ const VERSION = '5.2.8';
+ * SMTP line break constant.
+ * The SMTP port to use if one is not specified.
+ const DEFAULT_SMTP_PORT = 25;
+ * The maximum line length allowed by RFC 2822 section 2.1.1
+ const MAX_LINE_LENGTH = 998;
+ * The PHPMailer SMTP Version number.
+ * @see SMTP::VERSION
+ * SMTP server port number.
+ * @deprecated This is only ever used as a default value, so use the constant instead
+ * @see SMTP::DEFAULT_SMTP_PORT
+ public $SMTP_PORT = 25;
+ * SMTP reply line ending.
+ * @see SMTP::CRLF
+ * Debug output level.
+ * * `0` No output
+ * * `1` Commands
+ * * `2` Data and commands
+ * * `3` As 2 plus connection status
+ * * `4` Low-level data output
+ * * `echo` Output plain-text as-is, appropriate for CLI
+ * * `html` Output escaped, line breaks converted to <br>, appropriate for browser output
+ * * `error_log` Output to error log as configured in php.ini
+ * Whether to use VERP.
+ * @link http://www.postfix.org/VERP_README.html Info on VERP
+ * The timeout value for connection, in seconds.
+ * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2
+ * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
+ * @link http://tools.ietf.org/html/rfc2821#section-4.5.3.2
+ public $Timeout = 300;
+ * The SMTP timelimit value for reads, in seconds.
+ public $Timelimit = 30;
+ * The socket for the server connection.
+ protected $smtp_conn;
+ * Error message, if any, for the last call.
+ protected $error = array();
+ * The reply the server sent to us for HELO.
+ * If null, no HELO string has yet been received.
+ * @type string|null
+ protected $helo_rply = null;
+ * The most recent reply received from the server.
+ protected $last_reply = '';
+ * Output debugging info via a user-selected method.
+ * @param string $str Debug string to output
+ //Don't output, just log
+ //Cleans up output a bit for a better looking, HTML-safe output
+ echo htmlentities(
+ preg_replace('/[\r\n]+/', '', $str),
+ 'UTF-8'
+ . "<br>\n";
+ echo gmdate('Y-m-d H:i:s')."\t".trim($str)."\n";
+ * Connect to an SMTP server.
+ * @param string $host SMTP server IP or host name
+ * @param integer $port The port number to connect to
+ * @param integer $timeout How long to wait for the connection to open
+ * @param array $options An array of options for stream_context_create()
+ public function connect($host, $port = null, $timeout = 30, $options = array())
+ static $streamok;
+ //This is enabled by default since 5.0.0 but some providers disable it
+ //Check this once and cache the result
+ if (is_null($streamok)) {
+ $streamok = function_exists('stream_socket_client');
+ // Clear errors to avoid confusion
+ $this->error = array();
+ // Make sure we are __not__ connected
+ if ($this->connected()) {
+ // Already connected, generate error
+ $this->error = array('error' => 'Already connected to a server');
+ if (empty($port)) {
+ $port = self::DEFAULT_SMTP_PORT;
+ // Connect to the SMTP server
+ if ($this->do_debug >= 3) {
+ $this->edebug("Connection: opening to $host:$port, t=$timeout, opt=".var_export($options, true));
+ $errno = 0;
+ $errstr = '';
+ if ($streamok) {
+ $socket_context = stream_context_create($options);
+ //Suppress errors; connection failures are handled at a higher level
+ $this->smtp_conn = @stream_socket_client(
+ $host . ":" . $port,
+ $errno,
+ $errstr,
+ $timeout,
+ STREAM_CLIENT_CONNECT,
+ $socket_context
+ //Fall back to fsockopen which should work in more places, but is missing some features
+ $this->edebug("Connection: stream_socket_client not available, falling back to fsockopen");
+ $this->smtp_conn = fsockopen(
+ $port,
+ $timeout
+ // Verify we connected properly
+ if (!is_resource($this->smtp_conn)) {
+ $this->error = array(
+ 'error' => 'Failed to connect to server',
+ $this->edebug(
+ 'SMTP ERROR: ' . $this->error['error']
+ . ": $errstr ($errno)"
+ $this->edebug('Connection: opened');
+ // SMTP server can take longer to respond, give longer timeout for first read
+ // Windows does not have support for this timeout function
+ if (substr(PHP_OS, 0, 3) != 'WIN') {
+ $max = ini_get('max_execution_time');
+ if ($max != 0 && $timeout > $max) { // Don't bother if unlimited
+ @set_time_limit($timeout);
+ stream_set_timeout($this->smtp_conn, $timeout, 0);
+ // Get any announcement
+ $announce = $this->get_lines();
+ if ($this->do_debug >= 2) {
+ $this->edebug('SERVER -> CLIENT: ' . $announce);
+ * Initiate a TLS (encrypted) session.
+ public function startTLS()
+ if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
+ // Begin encrypted connection
+ if (!stream_socket_enable_crypto(
+ $this->smtp_conn,
+ true,
+ STREAM_CRYPTO_METHOD_TLS_CLIENT
+ )) {
+ * Perform SMTP authentication.
+ * Must be run after hello().
+ * @see hello()
+ * @param string $username The user name
+ * @param string $password The password
+ * @param string $authtype The auth type (PLAIN, LOGIN, NTLM, CRAM-MD5)
+ * @param string $realm The auth realm for NTLM
+ * @param string $workstation The auth workstation for NTLM
+ * @return boolean True if successfully authenticated.
+ public function authenticate(
+ $username,
+ $password,
+ $authtype = 'LOGIN',
+ $realm = '',
+ $workstation = ''
+ if (empty($authtype)) {
+ $authtype = 'LOGIN';
+ switch ($authtype) {
+ case 'PLAIN':
+ // Start authentication
+ if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
+ // Send encoded username and password
+ if (!$this->sendCommand(
+ 'User & Password',
+ base64_encode("\0" . $username . "\0" . $password),
+ 235
+ case 'LOGIN':
+ if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
+ if (!$this->sendCommand("Username", base64_encode($username), 334)) {
+ if (!$this->sendCommand("Password", base64_encode($password), 235)) {
+ case 'NTLM':
+ * ntlm_sasl_client.php
+ * Bundled with Permission
+ * How to telnet in windows:
+ * http://technet.microsoft.com/en-us/library/aa995718%28EXCHG.65%29.aspx
+ * PROTOCOL Docs http://curl.haxx.se/rfc/ntlm.html#ntlmSmtpAuthentication
+ require_once 'extras/ntlm_sasl_client.php';
+ $temp = new stdClass();
+ $ntlm_client = new ntlm_sasl_client_class;
+ //Check that functions are available
+ if (!$ntlm_client->Initialize($temp)) {
+ $this->error = array('error' => $temp->error);
+ 'You need to enable some modules in your php.ini file: '
+ . $this->error['error']
+ //msg1
+ $msg1 = $ntlm_client->TypeMsg1($realm, $workstation); //msg1
+ 'AUTH NTLM',
+ 'AUTH NTLM ' . base64_encode($msg1),
+ 334
+ //Though 0 based, there is a white space after the 3 digit number
+ //msg2
+ $challenge = substr($this->last_reply, 3);
+ $challenge = base64_decode($challenge);
+ $ntlm_res = $ntlm_client->NTLMResponse(
+ substr($challenge, 24, 8),
+ $password
+ //msg3
+ $msg3 = $ntlm_client->TypeMsg3(
+ $ntlm_res,
+ $realm,
+ $workstation
+ // send encoded username
+ return $this->sendCommand('Username', base64_encode($msg3), 235);
+ case 'CRAM-MD5':
+ if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
+ // Get the challenge
+ $challenge = base64_decode(substr($this->last_reply, 4));
+ // Build the response
+ $response = $username . ' ' . $this->hmac($challenge, $password);
+ // send encoded credentials
+ return $this->sendCommand('Username', base64_encode($response), 235);
+ * Calculate an MD5 HMAC hash.
+ * Works like hash_hmac('md5', $data, $key)
+ * in case that function is not available
+ * @param string $data The data to hash
+ * @param string $key The key to hash with
+ protected function hmac($data, $key)
+ if (function_exists('hash_hmac')) {
+ return hash_hmac('md5', $data, $key);
+ // The following borrowed from
+ // http://php.net/manual/en/function.mhash.php#27225
+ // RFC 2104 HMAC implementation for php.
+ // Creates an md5 HMAC.
+ // Eliminates the need to install mhash to compute a HMAC
+ // Hacked by Lance Rushing
+ $bytelen = 64; // byte length for md5
+ if (strlen($key) > $bytelen) {
+ $key = pack('H*', md5($key));
+ $key = str_pad($key, $bytelen, chr(0x00));
+ $ipad = str_pad('', $bytelen, chr(0x36));
+ $opad = str_pad('', $bytelen, chr(0x5c));
+ $k_ipad = $key ^ $ipad;
+ $k_opad = $key ^ $opad;
+ return md5($k_opad . pack('H*', md5($k_ipad . $data)));
+ * Check connection state.
+ * @return boolean True if connected.
+ if (is_resource($this->smtp_conn)) {
+ $sock_status = stream_get_meta_data($this->smtp_conn);
+ if ($sock_status['eof']) {
+ // the socket is valid but we are not connected
+ 'SMTP NOTICE: EOF caught while checking if connected'
+ $this->close();
+ return true; // everything looks good
+ * Close the socket and clean up the state of the class.
+ * Don't use this function without first trying to use QUIT.
+ * @see quit()
+ public function close()
+ $this->helo_rply = null;
+ // close the connection and cleanup
+ fclose($this->smtp_conn);
+ $this->edebug('Connection: closed');
+ * Send an SMTP DATA command.
+ * Issues a data command and sends the msg_data to the server,
+ * finializing the mail transaction. $msg_data is the message
+ * that is to be send with the headers. Each header needs to be
+ * on a single line followed by a <CRLF> with the message headers
+ * and the message body being separated by and additional <CRLF>.
+ * Implements rfc 821: DATA <CRLF>
+ * @param string $msg_data Message data to send
+ public function data($msg_data)
+ if (!$this->sendCommand('DATA', 'DATA', 354)) {
+ /* The server is ready to accept data!
+ * According to rfc821 we should not send more than 1000 characters on a single line (including the CRLF)
+ * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
+ * smaller lines to fit within the limit.
+ * We will also look for lines that start with a '.' and prepend an additional '.'.
+ * NOTE: this does not count towards line-length limit.
+ // Normalize line breaks before exploding
+ $lines = explode("\n", str_replace(array("\r\n", "\r"), "\n", $msg_data));
+ /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
+ * of the first line (':' separated) does not contain a space then it _should_ be a header and we will
+ * process all lines before a blank line as headers.
+ $field = substr($lines[0], 0, strpos($lines[0], ':'));
+ $in_headers = false;
+ if (!empty($field) && strpos($field, ' ') === false) {
+ $in_headers = true;
+ foreach ($lines as $line) {
+ $lines_out = array();
+ if ($in_headers and $line == '') {
+ // ok we need to break this line up into several smaller lines
+ //This is a small micro-optimisation: isset($str[$len]) is equivalent to (strlen($str) > $len)
+ while (isset($line[self::MAX_LINE_LENGTH])) {
+ //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
+ //so as to avoid breaking in the middle of a word
+ $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
+ if (!$pos) { //Deliberately matches both false and 0
+ //No nice break found, add a hard break
+ $pos = self::MAX_LINE_LENGTH - 1;
+ $lines_out[] = substr($line, 0, $pos);
+ $line = substr($line, $pos);
+ //Break at the found point
+ //Move along by the amount we dealt with
+ $line = substr($line, $pos + 1);
+ /* If processing headers add a LWSP-char to the front of new line
+ * RFC822 section 3.1.1
+ if ($in_headers) {
+ $line = "\t" . $line;
+ $lines_out[] = $line;
+ // Send the lines to the server
+ foreach ($lines_out as $line_out) {
+ //RFC2821 section 4.5.2
+ if (!empty($line_out) and $line_out[0] == '.') {
+ $line_out = '.' . $line_out;
+ $this->client_send($line_out . self::CRLF);
+ // Message data has been sent, complete the command
+ return $this->sendCommand('DATA END', '.', 250);
+ * Send an SMTP HELO or EHLO command.
+ * Used to identify the sending server to the receiving server.
+ * This makes sure that client and server are in a known state.
+ * Implements RFC 821: HELO <SP> <domain> <CRLF>
+ * and RFC 2821 EHLO.
+ * @param string $host The host name or IP to connect to
+ public function hello($host = '')
+ // Try extended hello first (RFC 2821)
+ return (boolean)($this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host));
+ * Low-level implementation used by hello()
+ * @param string $hello The HELO string
+ * @param string $host The hostname to say we are
+ protected function sendHello($hello, $host)
+ $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
+ $this->helo_rply = $this->last_reply;
+ return $noerror;
+ * Send an SMTP MAIL command.
+ * Starts a mail transaction from the email address specified in
+ * $from. Returns true if successful or false otherwise. If True
+ * the mail transaction is started and then one or more recipient
+ * commands may be called followed by a data command.
+ * Implements rfc 821: MAIL <SP> FROM:<reverse-path> <CRLF>
+ * @param string $from Source address of this message
+ public function mail($from)
+ $useVerp = ($this->do_verp ? ' XVERP' : '');
+ return $this->sendCommand(
+ 'MAIL FROM',
+ 'MAIL FROM:<' . $from . '>' . $useVerp,
+ 250
+ * Send an SMTP QUIT command.
+ * Closes the socket if there is no error or the $close_on_error argument is true.
+ * Implements from rfc 821: QUIT <CRLF>
+ * @param boolean $close_on_error Should the connection close if an error occurs?
+ public function quit($close_on_error = true)
+ $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
+ $err = $this->error; //Save any error
+ if ($noerror or $close_on_error) {
+ $this->error = $err; //Restore any error from the quit command
+ * Send an SMTP RCPT command.
+ * Sets the TO argument to $toaddr.
+ * Returns true if the recipient was accepted false if it was rejected.
+ * Implements from rfc 821: RCPT <SP> TO:<forward-path> <CRLF>
+ * @param string $toaddr The address the message is being sent to
+ public function recipient($toaddr)
+ 'RCPT TO',
+ 'RCPT TO:<' . $toaddr . '>',
+ array(250, 251)
+ * Send an SMTP RSET command.
+ * Abort any transaction that is currently in progress.
+ * Implements rfc 821: RSET <CRLF>
+ * @return boolean True on success.
+ public function reset()
+ return $this->sendCommand('RSET', 'RSET', 250);
+ * Send a command to an SMTP server and check its return code.
+ * @param string $command The command name - not sent to the server
+ * @param string $commandstring The actual command to send
+ * @param integer|array $expect One or more expected integer success codes
+ protected function sendCommand($command, $commandstring, $expect)
+ if (!$this->connected()) {
+ 'error' => "Called $command without being connected"
+ $this->client_send($commandstring . self::CRLF);
+ $reply = $this->get_lines();
+ $code = substr($reply, 0, 3);
+ $this->edebug('SERVER -> CLIENT: ' . $reply);
+ if (!in_array($code, (array)$expect)) {
+ $this->last_reply = null;
+ 'error' => "$command command failed",
+ 'smtp_code' => $code,
+ 'detail' => substr($reply, 4)
+ 'SMTP ERROR: ' . $this->error['error'] . ': ' . $reply
+ $this->last_reply = $reply;
+ * Send an SMTP SAML command.
+ * Starts a mail transaction from the email address specified in $from.
+ * Returns true if successful or false otherwise. If True
+ * commands may be called followed by a data command. This command
+ * will send the message to the users terminal if they are logged
+ * in and send them an email.
+ * Implements rfc 821: SAML <SP> FROM:<reverse-path> <CRLF>
+ * @param string $from The address the message is from
+ public function sendAndMail($from)
+ return $this->sendCommand('SAML', "SAML FROM:$from", 250);
+ * Send an SMTP VRFY command.
+ * @param string $name The name to verify
+ public function verify($name)
+ return $this->sendCommand('VRFY', "VRFY $name", array(250, 251));
+ * Send an SMTP NOOP command.
+ * Used to keep keep-alives alive, doesn't actually do anything
+ public function noop()
+ return $this->sendCommand('NOOP', 'NOOP', 250);
+ * Send an SMTP TURN command.
+ * This is an optional command for SMTP that this class does not support.
+ * This method is here to make the RFC821 Definition complete for this class
+ * and _may_ be implemented in future
+ * Implements from rfc 821: TURN <CRLF>
+ public function turn()
+ 'error' => 'The SMTP TURN command is not implemented'
+ $this->edebug('SMTP NOTICE: ' . $this->error['error']);
+ * Send raw data to the server.
+ * @param string $data The data to send
+ * @return integer|boolean The number of bytes sent to the server or false on error
+ public function client_send($data)
+ $this->edebug("CLIENT -> SERVER: $data");
+ return fwrite($this->smtp_conn, $data);
+ * Get the latest error.
+ public function getError()
+ return $this->error;
+ * Get the last reply from the server.
+ public function getLastReply()
+ return $this->last_reply;
+ * Read the SMTP server's response.
+ * Either before eof or socket timeout occurs on the operation.
+ * With SMTP we can tell if we have more lines to read if the
+ * 4th character is '-' symbol. If it is a space then we don't
+ * need to read anything else.
+ protected function get_lines()
+ // If the connection is bad, give up straight away
+ $data = '';
+ $endtime = 0;
+ stream_set_timeout($this->smtp_conn, $this->Timeout);
+ if ($this->Timelimit > 0) {
+ $endtime = time() + $this->Timelimit;
+ while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
+ $str = @fgets($this->smtp_conn, 515);
+ if ($this->do_debug >= 4) {
+ $this->edebug("SMTP -> get_lines(): \$data was \"$data\"");
+ $this->edebug("SMTP -> get_lines(): \$str is \"$str\"");
+ $data .= $str;
+ $this->edebug("SMTP -> get_lines(): \$data is \"$data\"");
+ // If 4th character is a space, we are done reading, break the loop, micro-optimisation over strlen
+ if ((isset($str[3]) and $str[3] == ' ')) {
+ // Timed-out? Log and break
+ $info = stream_get_meta_data($this->smtp_conn);
+ if ($info['timed_out']) {
+ 'SMTP -> get_lines(): timed-out (' . $this->Timeout . ' sec)'
+ // Now check if reads took too long
+ if ($endtime and time() > $endtime) {
+ 'SMTP -> get_lines(): timelimit reached ('.
+ $this->Timelimit . ' sec)'
+ * Enable or disable VERP address generation.
+ * @param boolean $enabled
+ public function setVerp($enabled = false)
+ $this->do_verp = $enabled;
+ * Get VERP address generation mode.
+ public function getVerp()
+ return $this->do_verp;
+ * Set debug output method.
+ * @param string $method The function/method to use for debugging output.
+ public function setDebugOutput($method = 'echo')
+ $this->Debugoutput = $method;
+ * Get debug output method.
+ public function getDebugOutput()
+ return $this->Debugoutput;
+ * Set debug output level.
+ * @param integer $level
+ public function setDebugLevel($level = 0)
+ $this->do_debug = $level;
+ * Get debug output level.
+ public function getDebugLevel()
+ return $this->do_debug;
+ * Set SMTP timeout.
+ * @param integer $timeout
+ public function setTimeout($timeout = 0)
+ $this->Timeout = $timeout;
+ * Get SMTP timeout.
+ public function getTimeout()
+ return $this->Timeout;
@@ -0,0 +1,148 @@
+ * CCMail Transporter
+class Transporter
+ * Default transporter instance name
+ * Get a transporter instance or create one
+ * Kill an instance to force the transporter to redo the construction
+ * The transporter handler name
+ * The transporter config array
+ * The transporter driver
+ * @var Transporter_Driver
+ protected $driver = null;
+ * Transporter instance constructor
+ $config = \CCConfig::create( 'mail' )->get( 'transporter.'.$name );
+ $config = \CCConfig::create( 'mail' )->get( 'transporter.'.$config );
+ // What driver should be used
+ 'driver' => 'sendmail',
+ // load the driver
+ $driver_class = __NAMESPACE__.'\\Transporter_'.ucfirst( $this->config->driver );
+ throw new Exception( "Invalid mail driver '".$this->config->driver."'" );
+ $this->driver = new $driver_class( $this->config );
+ * Forward the mail to the driver
+ * @param CCMail $mail
+ public function send( CCMail $mail )
+ $this->driver->send( $mail );
+ * Array Transporter
+ * This transporter is just for testing purposes
+class Transporter_Array implements Transporter_Interface
+ * Because this is just an array driver we
+ * simply store all send email in this array.
+ public static $store = array();
+ * Send the mail
+ * @param CCMail $mail The mail object.
+ * @throws Mail\Exception
+ static::$store[] = $mail->export_data();
+ * File Transporter
+ * The file transporter helps debugging in development
+ * The will not be sended but stored as file in the storage.
+class Transporter_File implements Transporter_Interface
+ $data = $mail->export_data();
+ $filename = 'mails/'.date( 'Y-m' ).'/'.date( 'd' ).'/'.date("H-i-s").'-'.\CCStr::clean_url( $data['subject'] ).'.log';
+ \CCFile::append( \CCStorage::path( $filename ), \CCJson::encode( $data, true ) );
@@ -0,0 +1,23 @@
+ * Transporter Interface
+interface Transporter_Interface
+ public function send( CCMail $mail );
@@ -0,0 +1,108 @@
+ * PHPMailer Transporter
+class Transporter_PHPMailer implements Transporter_Interface
+ * The Driver configuration
+ public $config;
+ * Recive and store the transporter configuration
+ public function __construct( $config )
+ $this->config = $config;
+ // create new phpmailer instance
+ $driver = new PHPMailer\PHPMailer();
+ // set the charset
+ $driver->CharSet = 'utf-8';
+ // set the xmailer tag
+ $driver->XMailer = "ClanCatsFramework 2.0 Mail / PHPMailer ".$driver->Version;
+ // get the mail data as array
+ $mail_data = $mail->export_data();
+ // from address and name
+ list( $from_email, $from_name ) = $mail_data['from'];
+ $driver->From = $from_email;
+ if ( !is_null( $from_name ) )
+ $driver->FromName = $from_name;
+ // set the recipients
+ foreach( $mail_data['to'] as $email => $name )
+ $driver->AddAddress( $email, $name );
+ // set bcc
+ foreach( $mail_data['bcc'] as $email => $name )
+ $driver->addBCC( $email, $name );
+ // set cc
+ foreach( $mail_data['cc'] as $email => $name )
+ $driver->addCC( $email, $name );
+ // add attachments
+ foreach( $mail_data['attachments'] as $path => $name )
+ $driver->addAttachment( $path, $name );
+ // set the subject
+ $driver->Subject = $mail_data['subject'];
+ // set the message
+ $driver->Body = $mail_data['message'];
+ $driver->AltBody = $mail_data['plaintext'];
+ $driver->IsHTML( !$mail_data['is_plaintext'] );
+ // setup the driver
+ $this->setup_driver( $driver );
+ if( !$driver->Send() )
+ throw new Exception( "Mail failure: ". $driver->ErrorInfo );
+ * Set the driver settings ( smtp / sendmail )
+ * @param PHPMailer $driver
+ protected function setup_driver( &$driver ) {}
+ * Sendmail Transporter
+class Transporter_Sendmail extends Transporter_PHPMailer
+ protected function setup_driver( &$driver )
+ if ( !is_null( $this->config->path ) )
+ $driver->Sendmail = $this->config->path;
+class Transporter_Smtp extends Transporter_PHPMailer
+ $driver->IsSMTP();
+ $driver->Host = $this->config->host;
+ // smtp auth?
+ if ( $this->config->auth === true )
+ $driver->SMTPAuth = true;
+ $driver->Username = $this->config->user;
+ $driver->Password = $this->config->pass;
+ $driver->SMTPSecure = $this->config->encryption;
+ $driver->Port = $this->config->port;
@@ -0,0 +1,124 @@
+<?php namespace Session;
+ * Session handler
+class CCSession
+ * Get an instance of a session manager
+ * @param string $manager
+ * @return Session\Manager
+ public static function manager( $manager = null )
+ return Manager::create( $manager );
+ * Get the fingerprint form an session
+ public static function fingerprint( $name = null )
+ return Manager::create( $name )->fingerprint();
+ * Check if the given fingerprint or the default fingerprint
+ * parameter matches the curent session fingerprint.
+ public static function valid_fingerprint( $fingerprint = null, $name = null )
+ return Manager::create( $name )->valid_fingerprint( $fingerprint );
+ * Get a value from the session
+ * @param string $default
+ public static function get( $key, $default, $manager = null )
+ return Manager::create( $manager )->get( $key, $default );
+ * Get a value from the session and remove it afterwards
+ public static function once( $key, $default, $manager = null )
+ return Manager::create( $manager )->once( $key, $default );
+ * Set a value on the session
+ public static function set( $key, $value, $manager = null )
+ return Manager::create( $manager )->set( $key, $value );
+ * Similar to add but forces the element to be an array
+ * and appends an item.
+ public static function add( $key, $value, $manager = null )
+ return Manager::create( $manager )->add( $key, $value );
+ * Has a value on the session
+ public static function has( $key, $manager = null )
+ return Manager::create( $manager )->has( $key );
+ * Delete a value on the session
+ public static function delete( $key, $manager = null )
+ return Manager::create( $manager )->delete( $key );
+ * Session Exceptions
+class Exception extends \Exception {}
@@ -0,0 +1,358 @@
+ * Session Manager
+class Manager extends \CCDataObject
+ * Default session instance name
+ * Get a session instance manager
+ * Some default values for our session
+ public static function default_data_provider()
+ 'last_active' => time(),
+ 'current_lang' => \CCLang::current(),
+ 'client_agent' => \CCServer::client( 'agent' ),
+ 'client_ip' => \CCServer::client( 'ip' ),
+ 'client_port' => \CCServer::client( 'port' ),
+ 'client_lang' => \CCServer::client( 'language' ),
+ * The session manager name
+ protected $_name = null;
+ * The session config array
+ protected $_config = null;
+ * The session driver
+ * @var Manager_Driver
+ protected $_driver = null;
+ * Session ID
+ public $id;
+ * The Fingerprint
+ public $fingerprint;
+ * Session constructor
+ protected function __construct( $name, $config = null )
+ $config = \CCConfig::create( 'session' )->get( $name );
+ $config = \CCConfig::create( 'session' )->get( $config );
+ if ( empty( $config ) )
+ throw new Exception( "Session\\Manager::create - Invalid session manager (".$name.")." );
+ $this->_name = $name;
+ // keep the configuration array
+ $this->_config = $config;
+ $driver_class = __NAMESPACE__."\\Manager_".ucfirst( $config['driver'] );
+ throw new Exception( "Session\\Manager::create - The driver (".$driver_class.") is invalid." );
+ // try to get the id from cookie
+ $this->id = $this->cookie_session_id();
+ // set the fingerprint
+ $this->fingerprint = sha1( $this->id );
+ // Before reading we might have to kill old sessions using
+ // the Garbage collector
+ if ( \CCArr::get( 'gc.enabled', $this->_config, true ) )
+ if ( mt_rand( 1, \CCArr::get( 'gc.factor', $this->_config, 25 ) ) == 1 )
+ $this->gc();
+ // Register a shutdown event to write the session down
+ // This should not happen on shutdown if we using command line
+ if ( !\ClanCats::is_cli() )
+ \CCEvent::mind( 'CCF.shutdown', array( $this, 'write' ) );
+ // Now get the inital data from our driver
+ $this->read();
+ * @param string $driver The full driver class ( Session\Manager_ExampleDriver )
+ $this->_driver = new $driver( $this->_name, $this->_config );
+ * Return the default data for the session
+ protected function default_data()
+ return call_user_func( \ClanCats::$config->get( 'session.default_data_provider' ) );
+ * Get the current cookie name
+ protected function cookie_name()
+ return $this->_name.\CCArr::get( 'cookie_suffix', $this->_config, '-ccf-session' );
+ * Get the current session id from the cookie
+ protected function cookie_session_id()
+ return \CCCookie::get( $this->cookie_name(), false );
+ * Retrive the current session fingerprint
+ public function fingerprint()
+ return $this->fingerprint;
+ * Does the current session fingerprint match a parameter
+ * When no parameter is given we use GET->s as default parameter
+ * @param string $fingerprint
+ public function valid_fingerprint( $fingerprint = null )
+ if ( is_null( $fingerprint ) )
+ $fingerprint = \CCIn::get( \ClanCats::$config->get( 'session.default_fingerprint_parameter' ), false );
+ return $this->fingerprint === $fingerprint;
+ * Get a value from data and remove it afterwards
+ * @param mixed $default
+ public function once( $key, $default = null )
+ $value = \CCArr::get( $key, $this->_data, $default );
+ \CCArr::delete( $key, $this->_data );
+ * Read data from the session driver. This overwrite's
+ * any changes made on runtime.
+ public function read()
+ // Do we already have a session id if not we regenerate
+ // the session and assign the default data.
+ if ( $this->id )
+ if ( !$this->_data = $this->_driver->read( $this->id ) )
+ $this->regenerate();
+ $this->_data = array();
+ if ( !is_array( $this->_data ) )
+ $this->_data = array_merge( $this->_data, $this->default_data() );
+ $this->_data = $this->default_data();
+ * Write the session to the driver
+ public function write()
+ $this->_driver->write( $this->id, $this->_data );
+ // We also have to set the cookie again to keep it alive
+ \CCCookie::set( $this->cookie_name(), $this->id, \CCArr::get( 'lifetime', $this->_config, \CCDate::minutes( 5 ) ) );
+ * Generate a new session id and checks the dirver for dublicates.
+ * @return string The new generated session id.
+ public function regenerate()
+ do
+ $id = \CCStr::random( 32 );
+ while ( $this->_driver->has( $id ) );
+ $this->fingerprint = sha1( $id );
+ return $this->id = $id;
+ * Destory the session an create a new one
+ public function destroy()
+ return $this->regenerate();
+ * Garbage collection, delete all outdated sessions
+ public function gc()
+ $lifetime = \CCArr::get( 'lifetime', $this->_config, \CCDate::minutes( 5 ) );
+ if ( $lifetime < ( $min_lifetime = \CCArr::get( 'min_lifetime', $this->_config, \CCDate::minutes( 5 ) ) ) )
+ $lifetime = $min_lifetime;
+ $this->_driver->gc( $lifetime );
@@ -0,0 +1,68 @@
+ * Session Array test driver
+class Manager_Array implements Manager_Interface
+ * the data holder
+ private $data = array();
+ * Read data from the session dirver
+ * @param string $id The session id key.
+ public function read( $id )
+ if ( array_key_exists( $id, $this->data ) )
+ return $this->data[$id];
+ * Check if a session with the given key already exists
+ public function has( $id )
+ return array_key_exists( $id, $this->data );
+ public function write( $id, $data )
+ $this->data[$id] = $data;
+ * Delete session that are older than the given time in secounds
+ * @param int $time
+ public function gc( $time )
+ $this->data = array();
@@ -0,0 +1,86 @@
+ * Session Cookie Driver
+class Manager_Cookie implements Manager_Interface
+ * Cookie suffix string
+ private $cookie_suffix = null;
+ * Salt for crypting the session
+ private $crypt_salt = null;
+ * Cookie driver constructor
+ public function __construct( $name, $conf )
+ $this->cookie_suffix = \CCArr::get( 'cookie_suffix', $conf, '-session-store' );
+ $this->crypt_salt = \CCArr::get( 'crypt_salt', $conf );
+ if ( $this->has( $id ) )
+ return json_decode( \CCCrypter::decode( \CCCookie::get( $id.$this->cookie_suffix ), $this->crypt_salt ), true );
+ * @return boool
+ return (bool) \CCCookie::get( $id.$this->cookie_suffix, false );
+ \CCCookie::set( $id.$this->cookie_suffix, \CCCrypter::encode( json_encode( $data ), $this->crypt_salt ) );
+ return false; // There is no garbage collection when using cookies
@@ -0,0 +1,161 @@
+ * Session Database Driver
+class Manager_Database implements Manager_Interface
+ * The database instance used to store the sessions
+ private $database = null;
+ * The database table used
+ private $table = null;
+ * An array of fields that get used as columns
+ private $index_fields = null;
+ * The ID the session started with
+ private $inital_sesssion_id = null;
+ $this->database = \CCArr::get( 'database', $conf );
+ $this->table = \CCArr::get( 'table', $conf, 'sessions' );
+ $this->index_fields = array_merge( \CCArr::get( 'index_fields', $conf, array() ), array_keys( Manager::default_data_provider() ) );
+ $this->inital_session_id = $id;
+ if ( $data = \DB::select( $this->table )->where( 'id', $id )->one( $this->database ) )
+ $session_data = unserialize( $data->content );
+ foreach( $this->index_fields as $field )
+ $session_data[$field] = $data->$field;
+ return $session_data;
+ return \DB::select( $this->table )
+ ->where( 'id', $id )
+ ->column( 'id', $this->database ) == $id;
+ $columns = array();
+ $columns[$field] = $data[$field]; unset( $data[$field] );
+ $columns['content'] = serialize( $data );
+ // When the session id didnt change we will do an update
+ if ( $id == $this->inital_session_id )
+ // because the we might already be in the shutdown process we
+ // have to forward sql error directly to CCError
+ \DB::update( $this->table, $columns )
+ ->run( $this->database );
+ } catch ( \Exception $e )
+ \CCError::exception( $e );
+ // else insert a new one
+ // add the id to the columns
+ $columns['id'] = $id;
+ \DB::insert( $this->table, $columns )->run( $this->database );
+ // if we don't have the last active key we cannot execute
+ // the garbage collection
+ if ( !in_array( 'last_active', $this->index_fields ) )
+ \DB::delete( $this->table )
+ ->where( 'last_active', '<', time() - $time )
@@ -0,0 +1,87 @@
+ * Session File Driver
+class Manager_File implements Manager_Interface
+ return unserialize( \CCFile::read( $this->file_path( $id ) ) );
+ return file_exists( $this->file_path( $id ) );
+ if ( !\CCFile::write( $this->file_path( $id ), serialize( $data ) ) )
+ \CCError::exception( new Exception( 'Could not write session file.' ) );
+ foreach( \CCFile::ls( \CCStorage::path( 'sessions/*' ) ) as $file )
+ if ( ( filemtime( $file ) - ( time() - $time ) ) < 0 )
+ if ( !\CCFile::delete( $file ) )
+ throw new Exception( "Manager_File::gc - cannot delete session file." );
+ * Get the file path by id
+ * @param string $id
+ private function file_path( $id )
+ return \CCStorage::path( 'sessions/'.$id.'.session' );
@@ -0,0 +1,46 @@
+ * Session Manager Driver Interface
+interface Manager_Interface
+ public function read( $id );
+ public function has( $id );
+ public function write( $id, $data );
+ public function gc( $time );
+class Manager_Json implements Manager_Interface
+ return \CCJson::read( $this->file_path( $id ) );
+ \CCJson::write( $this->file_path( $id ), $data, true );
+ return \CCStorage::path( 'sessions/'.$id.'.json' );
@@ -0,0 +1,109 @@
+<?php namespace UI;
+ * Uiify component Builder
+ * @package Uiify
+ * @version 1.0
+ * @copyright 2013 ClanCats GmbH
+class Alert
+ * notification holder
+ protected static $_alerts = array();
+ * available alert types
+ protected static $_types = array(
+ 'danger',
+ 'warning',
+ 'info',
+ 'success',
+ * When loading the alerts we going to add alerts
+ * from the previous request stored in the session
+ if ( \CCSession::has( 'ui.alerts' ) )
+ static::$_alerts = \CCSession::once( 'ui.alerts', array() );
+ * Validate the alert type and format the message
+ * @param string|array $message
+ private static function prepare( $type, $message )
+ // to avoid typos and other mistakes we
+ // validate the alert type
+ $type = strtolower( $type );
+ if ( !in_array( $type, static::$_types ) )
+ throw new Exception( "UI\Alert - Unknown alert type '{$type}'!" );
+ // We always need to return an array
+ if ( !is_array( $message ) )
+ return array( $message );
+ * Flash an alert
+ * This will add the alert to session so it gets displayed on the next request.
+ public static function flash( $type, $message )
+ \CCSession::add( 'ui.alerts.'.$type, static::prepare( $type, $message ) );
+ * Add an alert to the current queue
+ public static function add( $type, $message )
+ static::$_alerts[$type][] = static::prepare( $type, $message );
+ * Render the alerts and reset the queue
+ * @return UI\HTML
+ public static function render()
+ $html = Builder::handle( 'alert', static::$_alerts );
+ static::$_alerts = array();
+ return $html;
@@ -0,0 +1,55 @@
+ * The current Bulder object
+ * @var UI\Builder_Interface
+ protected static $builder = null;
+ * The user interface configuration
+ // select the current builder
+ $builder_class = \ClanCats::$config->get( 'ui.builder', "\\UI\\Builder_Bootstrap" );
+ static::$builder = new $builder_class;
+ // load the ui configuration
+ static::$config = \CCConfig::create( 'ui' );
+ * Handle a build request
+ * @param mixed ...
+ public static function handle()
+ $args = func_get_args();
+ $key = array_shift( $args );
+ return call_user_func_array( array( static::$builder, 'build_'.$key ), $args );
@@ -0,0 +1,99 @@
+ * Uiify Bootstrap Builder class
+ * @version 0.1
+class Builder_Bootstrap implements Builder_Interface
+ * Build an input form
+ * @param UI\HTML $element
+ public function build_form_input( $element )
+ return $element->class( 'form-control' );
+ public function build_form_textarea( $element )
+ * Build an input label
+ public function build_form_label( $element )
+ return $element->class( 'control-label' );
+ public function build_form_checkbox( $element )
+ return HTML::tag( 'div', $element->render() )->class( 'checkbox' );
+ public function build_form_select( $element )
+ * Build the UI alerts
+ * @param array $alerts
+ public function build_alert( $alerts )
+ return HTML::tag( 'div', function() use( $alerts )
+ // loop trough all alert types
+ foreach( $alerts as $type => $items )
+ foreach( $items as $alert )
+ $alert = implode( "<br>\n", $alert );
+ // close button
+ $close = HTML::tag( 'button', '×' )
+ ->add_class( 'close' )
+ ->type( 'button' )
+ ->data( 'dismiss', 'alert' );
+ // alert div
+ echo HTML::tag( 'div', $close.$alert )
+ ->add_class( 'alert fade in' )
+ ->add_class( 'alert-'.$type );
+ })->add_class( 'ui-alert-container' );
+ * Uiify Builder Interface
+interface Builder_Interface
+ public function build_form_input( $element );
+ public function build_form_label( $element );
+ public function build_form_checkbox( $element );
+ public function build_alert( $alerts );
@@ -0,0 +1,151 @@
+ * Uiify Base Element
+class E {
+ * generates an html tag
+ * @return UI\E
+ public static function create( $name, $param1 = null, $param2 = null )
+ return new static( $name, $param1, $param2 );
+ * Create elemnts by dynamic calls
+ * @param array $arguments
+ public static function __callStatic( $name, $arguments )
+ array_unshift( $arguments, $name );
+ return forward_static_call_array( array( "UI\\E", 'create' ), $arguments );
+ * generates element attribute string
+ * @param array $attr
+ public static function attr( $attr = array() ) {
+ $buffer = " ";
+ foreach( $attr as $key => $value ) {
+ $buffer .= $key.'="'.$value.'" ';
+ return substr( $buffer, 0, -1 );
+ * The element value / content
+ * the elements attributes
+ public $attr = array();
+ * the element name
+ public $name = "";
+ * element constructor
+ public function __construct( $name, $param1 = null, $param2 = null ) {
+ * Dynamic params
+ if ( is_array( $param1 ) ) {
+ $this->attr = $param1;
+ if ( is_string( $param2 ) || is_closure( $param2 ) ) {
+ $this->value = $param2;
+ elseif ( is_string( $param1 ) || is_closure( $param1 ) ) {
+ $this->value = $param1;
+ if ( is_array( $param2 ) ) {
+ $this->attr = $param2;
+ * Set an attribute
+ * @param string $attribute
+ * @return $this
+ public function set_attr( $attribute, $value = null ) {
+ if ( is_null( $value ) || $value === false ) {
+ if ( array_key_exists( $attribute, $this->attr ) ) {
+ unset( $this->attr[$attribute] );
+ if ( $value === true ) {
+ $value = $attribute;
+ $this->attr[ $attribute ] = $value;
+ * magic call to set attributes
+ * @param array $args
+ function __call( $method, $args ) {
+ return $this->set_attr( $method, $args[key($args)] );
+ * to string magic
+ public function __toString() {
+ return $this->render();
+ * render this element
+ public function render() {
+ // execute callback if we have one first
+ if ( is_closure( $this->value ) ) {
+ ob_start(); $return = call_user_func( $this->value ); $this->value = ob_get_clean().$return;
+ return '<'.$this->name.static::attr( $this->attr ).
+ ( !is_null( $this->value ) ? '>'.$this->value.'</'.$this->name.'>' : ' />' );
+ * UI Exception
@@ -0,0 +1,369 @@
+ * Uiify Form Generator
+class Form
+ * Current prefix
+ private static $id_prefix = null;
+ * Registerd patterns
+ * @var array[callbacks]
+ private static $macros = array();
+ * Is the builder enabled
+ private static $builder_enabled = false;
+ * Here we register all inital macros
+ static::$builder_enabled = Builder::$config->get( 'form.builder_enabled' );
+ * Create a new static pattern for the form generator.
+ * I found this Macro idea in the wonderfull laravel framework thanks.
+ public static function macro( $key, $callback )
+ static::$macros[$key] = $callback;
+ * Open a new form
+ * This will set the current form to this one
+ public static function start( $key, $attr = array() )
+ $attributes = array();
+ // force the form role
+ $attributes['role'] = 'form';
+ if ( !is_null( $key ) )
+ static::$id_prefix = $attributes['id'] = static::form_id( 'form', $key );
+ $attributes = array_merge( $attributes, $attr );
+ return '<form'.HTML::attr( $attributes ).'>';
+ * Closes the form and resest the current form
+ public static function end()
+ static::$id_prefix = null; return '</form>';
+ * Create a new from instance
+ * @param string $key The form key used for identification.
+ * @param array $attr The form dom attributes.
+ * @return UI\Form
+ public static function capture( $callback = null, $key = null, $attr = null )
+ // we got some dynamics in the parameters here so in case
+ // of this shift stuff
+ if ( is_callable( $attr ) && !is_callable( $callback ) )
+ $new_attr = $key;
+ $key = $callback;
+ $callback = $attr;
+ $attr = $new_attr;
+ $form = new static;
+ if ( is_null( $callback ) )
+ throw new Exception( 'Cannot use capture without a callback or string given.' );
+ // fix no array given
+ if ( !is_array( $attr ) )
+ $attr = array();
+ return static::start( $key, $attr ).\CCStr::capture( $callback, array( $form ) ).static::end();
+ * Format an id by configartion
+ * @param string $type element, form etc..
+ public static function form_id( $type, $name )
+ return sprintf( Builder::$config->get( 'form.'.$type.'_id_format' ), $name );
+ * Format an id by configartion with the current form prefix
+ * @param strgin $name
+ public static function build_id( $type, $name )
+ if ( !is_null( static::$id_prefix ) )
+ return static::$id_prefix.'-'.static::form_id( $type, $name );
+ return static::form_id( $type, $name );
+ * Forward intance functions to static using the current instance
+ public static function __callStatic( $method, $args )
+ // take the first argument and add it again as the id
+ array_unshift( $args, static::build_id( $method, reset( $args ) ) );
+ if ( array_key_exists( $method, static::$macros ) )
+ // execute the macro
+ return call_user_func_array( static::$macros[$method], $args );
+ if ( method_exists( __CLASS__, 'make_'.$method ) )
+ return call_user_func_array( array( __CLASS__, 'make_'.$method ), $args );
+ throw new Exception( "UI\\Form - Unknown macro '".$method."'." );
+ * Simply forward to call static to allow execution from
+ * object context
+ public function __call( $method, $args )
+ return static::__callStatic( $method, $args );
+ * make an input
+ * @param string $id The id that has been generated for us.
+ * @param string $key This is the name
+ public static function make_input( $id, $key, $value = null, $type = 'text', $attr = array() )
+ $element = HTML::tag( 'input', array_merge( array(
+ 'id' => $id,
+ 'name' => $key,
+ 'type' => $type
+ ), $attr ));
+ if ( !is_null( $value ) )
+ $element->value( _e( $value ) );
+ if ( !static::$builder_enabled )
+ return $element;
+ return Builder::handle( 'form_input', $element );
+ * make a label
+ public static function make_label( $id, $key, $text = null, $attr = array() )
+ if ( is_null( $text ) )
+ $text = $key;
+ $element = HTML::tag( 'label', $text, array_merge( array(
+ 'for' => static::build_id( 'input', $key )
+ return Builder::handle( 'form_label', $element );
+ * make a checkbox
+ * @param bool $active Is the checkbox cheked
+ public static function make_checkbox( $id, $key, $text = '', $active = false, $attr = array() )
+ 'type' => 'checkbox'
+ $element->checked( (bool) $active );
+ $element = HTML::tag( 'label', $element->render().' '.$text );
+ return Builder::handle( 'form_checkbox', $element );
+ * generate a textarea
+ public static function make_textarea( $id, $key, $value = '', $attr = array() )
+ $element = HTML::tag( 'textarea', _e( $value ), array_merge( array(
+ return Builder::handle( 'form_textarea', $element );
+ * generate a file input
+ public static function make_file( $id, $key, $attr = array() )
+ return static::make_input( $id, $key, null, 'file', $attr );
+ * generate an select
+ * Form::select( 'gender', array( 'F', 'M' ), 0 );
+ * Form::select( 'gender', array( '1' => 'A', '2' => 'B' ), array( 1,2 ), 2 );
+ * @param string $name This is the name
+ * @param array $options
+ * @param array $selected
+ public static function make_select( $id, $name, array $options, $selected = array(), $size = 1 )
+ if ( !is_array( $selected ) )
+ $selected = array( $selected );
+ $buffer = "";
+ foreach( $options as $key => $value )
+ $option = HTML::tag( 'option', $value )
+ ->value( $key );
+ if ( in_array( $key, $selected ) )
+ $option->selected( true );
+ $buffer .= $option->render();
+ $element = HTML::tag( 'select', $buffer, array(
+ 'name' => $name,
+ 'size' => $size,
+ ) );
+ return Builder::handle( 'form_select', $element );
@@ -0,0 +1,135 @@
+ * Uiify Base HTML stuff
+class HTML extends E
+ * The maker is like a development shortcut to create
+ * html elements on the go
+ * If param2 is set it wil be used as the content otherwise
+ * the the first parameter will be splittet by the first space.
+ * @param string $param
+ * @param string $param2
+ public static function maker( $param, $param2 = null )
+ return static::create( $param, $param2 );
+ $param = explode( ' ', $param );
+ $element = array_shift( $param );
+ return static::create( $element, implode( ' ', $param ) );
+ * generates html attribute string
+ switch( $key ) {
+ case 'class':
+ if ( is_array( $value ) ) { $value = implode( ' ', $value ); }
+ case 'style':
+ if ( is_array( $value ) ) {
+ $style = $value; $value = "";
+ foreach( $style as $k => $v ) {
+ $value .= $k.':'.$v.';';
+ public static function tag( $name, $param1 = null, $param2 = null )
+ return static::create( $name, $param1, $param2 );
+ * set an data attribute
+ public function data( $key, $value ) {
+ return $this->set_attr( 'data-'.$key, $value );
+ * add html class
+ * @param string $class
+ public function add_class( $class ) {
+ if ( !isset( $this->attr['class'] ) || !is_array( $this->attr['class'] ) ) {
+ $this->_sanitize_class();
+ if ( strpos( $class, ' ' ) !== false ) {
+ $class = explode( ' ', $class );
+ if ( is_string( $class ) ) {
+ $class = array( $class );
+ foreach ( $class as $c ) {
+ $this->attr['class'][] = $c;
+ * remove html class
+ public function remove_class( $class ) {
+ $this->attr['class'] = array_diff( $this->attr['class'], array( $class ) );
+ * clean the classes attribute
+ private function _sanitize_class() {
+ if ( isset( $this->attr['class'] ) && is_string( $this->attr['class'] ) ) {
+ $this->attr['class'] = explode( ' ', $this->attr['class'] );
+ $this->attr['class'] = array();
@@ -0,0 +1,125 @@
+ * Uiify Table Generator
+class Table {
+ * get an table instance
+ public static function create( $attr = array() ) {
+ return new static( $attr );
+ * creates an cell
+ public static function cell( $value, $attr = array(), $ele = 'td' ) {
+ return HTML::tag( $ele, $value, $attr );
+ * The table element
+ public $element = null;
+ * the table header
+ public $header = array();
+ * data holder
+ public $data = array();
+ * generate
+ public function __construct( $attr = array() ) {
+ $this->element = HTML::create( 'table', $attr );
+ * add a new row
+ public function row( $data = array(), $attr = array() ) {
+ $this->data[] = $this->_repair_row( $data, $attr ); return $this;
+ * set the header
+ public function header( $data = array(), $attr = array() ) {
+ $this->header = $this->_repair_row( $data, $attr ); return $this;
+ * repair a row for rendering
+ private function _repair_row( $data = array(), $attr = array() ) {
+ $row = array();
+ foreach( $data as $key => $value ) {
+ if ( !$value instanceof HTML || ( $value->name != 'td' && $value->name != 'th' ) ) {
+ $value = static::cell( (string) $value );
+ if ( is_string( $key ) ) {
+ $row[$key] = $value;
+ $row[] = $value;
+ return array( $row, $attr );
+ * magic to string
+ * generate the output
+ $buffer = '';
+ if ( !empty( $this->header ) ) {
+ $buffer .= '<tr'.HTML::attr( $this->header[1] ).'>';
+ foreach( $this->header[0] as $head ) {
+ $head->name = 'th';
+ $buffer .= $head->render();
+ $buffer .= '</tr>';
+ foreach( $this->data as $row ) {
+ $buffer .= '<tr'.HTML::attr( $row[1] ).'>';
+ foreach( $this->header[0] as $key => $value ) {
+ $buffer .= HTML::tag( 'td', $row[0][$key]->value, $row[0][$key]->attr );
+ else {
+ foreach( $row[0] as $value ) {
+ $buffer .= $value->render();
+ $this->element->value = $buffer;
+ return $this->element->render();
+ * Database Bundle
+ * Here we define the database interface shadow and namespace
+// namepace
+\CCFinder::map( 'DB', COREPATH.'bundles/Database/' );
+// and the shdaow
+\CCFinder::shadow( 'DB', 'DB', COREPATH.'bundles/Database/DB'.EXT );
+ * UI Bundle
+ * The UI Bundle contains some helpers to generate HTML.
+\CCFinder::map( 'UI', COREPATH.'bundles/UI/' );
+ * Session Bundle
+ * Session managment bundle
+\CCFinder::map( 'Session', COREPATH.'bundles/Session/' );
+\CCFinder::shadow( 'CCSession', 'Session', COREPATH.'bundles/Session/CCSession'.EXT );
+ * Authentication Bundle
+ * The Authentication bundle for basic a basic user and login
+\CCFinder::map( 'Auth', COREPATH.'bundles/Auth/' );
+\CCFinder::shadow( 'CCAuth', 'Auth', COREPATH.'bundles/Auth/CCAuth'.EXT );
+ * Email Bundle
+ * The Email bundle mostly wraps phpmailer
+\CCFinder::map( 'Mail', COREPATH.'bundles/Mail/' );
+// phpmailer
+\CCFinder::bind( array(
+ "Mail\\PHPMailer\\PHPMailer" => COREPATH.'bundles/Mail/PHPMailer/class.phpmailer'.EXT,
+ "Mail\\PHPMailer\\POP3" => COREPATH.'bundles/Mail/PHPMailer/class.php3'.EXT,
+ "Mail\\PHPMailer\\SMTP" => COREPATH.'bundles/Mail/PHPMailer/class.smtp'.EXT,
+));
+\CCFinder::shadow( 'CCMail', 'Mail', COREPATH.'bundles/Mail/CCMail'.EXT );
@@ -0,0 +1,57 @@
+<?php namespace Core;
+ * App object
+ * The application object implements some events, in the most
+ * cases your routes are app specific so return them in your App class.
+class CCApp
+ public static $name = 'CCF Application';
+ * Get the applications name
+ public static function name()
+ return static::$name;
+ * This function should provide the application routes.
+ * By default its going to use the router.config file.
+ public static function routes()
+ return CCConfig::create( ClanCats::$config->get( 'router.map' ) )->raw();
+ * You can return a CCResponse wich will cancle all
+ * other actions if its enebaled ( see. main.config -> send_app_wake_response )
+ // do stuff
@@ -0,0 +1,455 @@
+ * Array
+ * php array helpers
+class CCArr
+ * Get the first element of an array
+ public static function first( $arr )
+ return array_shift( $arr );
+ * Get the last element of an array
+ public static function last( $arr )
+ return array_pop( $arr );
+ * Adds a single item or array of items at the end of the referenced array
+ * If you want to combine multiple arrays recursivly or use key => value pairs, please use CCArr::merge()
+ * Example:
+ * $bar = array( 'bar' );
+ * CCArr::push( 'foo', $bar ); // $bar = array( 'bar', 'foo' )
+ * CCArr::push( array( 'foo', 'baz' ), $bar ); // $bar = array( 'bar', array( 'foo', 'baz' ) )
+ * CCArr::push( array( 'foo', 'baz' ), $bar, true ); // $bar = array( 'bar', 'foo', 'baz' )
+ * @param mixed $item The item you would like to add to the array
+ * @param array $array The input array by reference
+ * @param bool $merge If $merge is set to true, push will merge each element of $item into $array
+ public static function push( $item, &$arr, $merge = false )
+ if( !is_array( $arr ) )
+ throw new \InvalidArgumentException('CCArr::push - second argument has to be an array.');
+ if ( $merge && is_array( $item ) )
+ foreach ( $item as $value )
+ $arr[] = $value;
+ return $arr;
+ $arr[] = $item;
+ * Adds an item to an element in the array
+ * CCArr::add( 'foo.bar', 'test' );
+ * Results:
+ * array( 'foo' => array( 'bar' => array( 'test' ) ) )
+ public static function add( $key, $item, &$arr )
+ throw new \InvalidArgumentException('CCArr::add - second argument has to be an array.');
+ if ( !is_array( static::get( $key, $arr ) ) )
+ return static::set( $key, array( $item ), $arr );
+ return static::set( $key, static::push( $item, static::get( $key, $arr ) ), $arr );
+ * get a special item from every array
+ * @param array[array] $arr
+ public static function pick( $key, $arr )
+ throw new \InvalidArgumentException('CCArr::pick - second argument has to be an array.');
+ $return = array();
+ foreach( $arr as $array )
+ $return[] = CCArr::get( $key, $array );
+ return $return;
+ * Check if an array is multidimensional
+ * Elements with empty arrays doesn't count!
+ * CCArr::is_multi( array( 'foo', array( 'bar', 'baz' ) ) ) === true
+ * CCArr::is_multi( array( array() ) ) === false
+ * CCArr::is_multi( false ) === false
+ * @param array $arr
+ public static function is_multi( $arr )
+ // if $arr isn't an array both count() will return useless values 0 (count(null)) or 1 (count(false)) and so the function will return false
+ if ( count( $arr ) == count( $arr, COUNT_RECURSIVE ) )
+ * Check if first element of an array is an array
+ * CCArr::is_collection( array( 'foo', array( 'bar', 'baz' ) ) ) === false
+ * CCArr::is_collection( array( array() ) ) === true
+ * CCArr::is_collection( false ) // Exception
+ public static function is_collection( $arr )
+ return is_array( reset( $arr ) );
+ * sum items in an array or use special item in the array
+ public static function sum( $arr, $key = null )
+ throw new \InvalidArgumentException('CCArr::sum - first argument has to be an array.');
+ $sum = 0;
+ if ( is_string( $key ) && CCArr::is_multi( $arr ) )
+ $arr = CCArr::pick( $key, $arr );
+ foreach ( $arr as $item )
+ if ( is_numeric( $item ) )
+ $sum += $item;
+ return $sum;
+ * get the average of the items
+ public static function average( $arr, $key = null )
+ throw new \InvalidArgumentException('CCArr::average - first argunent has to be an array.');
+ return ( static::sum( $arr ) / count( $arr ) );
+ * create an object from an array
+ * @return object
+ public static function object( $arr )
+ throw new \InvalidArgumentException("CCArr::object - only arrays can be passed.");
+ $object = new \stdClass();
+ if ( !empty( $arr ) )
+ foreach ( $arr as $name => $value)
+ if ( is_array( $value ) )
+ $value = static::object( $value );
+ $object->$name = $value;
+ return $object;
+ * merge arrays recursivly together
+ * @param array $array ...
+ public static function merge()
+ // get all arguments
+ $arrs = func_get_args();
+ foreach ( $arrs as $arr )
+ if ( !is_array( $arr ) )
+ throw new \InvalidArgumentException('CCArr::merge - all arguments have to be arrays.');
+ foreach ( $arr as $key => $value )
+ if ( array_key_exists( $key, $return ) )
+ if ( is_array( $value ) && is_array( $return[$key] ) )
+ $value = static::merge( $return[$key], $value );
+ $return[$key] = $value;
+ * return an item from an array with dottet dimensions
+ public static function get( $key, $arr, $default = null )
+ if ( isset( $arr[$key] ) )
+ return $arr[$key];
+ if ( strpos( $key, '.' ) !== false )
+ $kp = explode( '.', $key );
+ switch ( count( $kp ) )
+ case 2:
+ if ( isset( $arr[$kp[0]][$kp[1]] ) )
+ return $arr[$kp[0]][$kp[1]];
+ case 3:
+ if ( isset( $arr[$kp[0]][$kp[1]][$kp[2]] ) )
+ return $arr[$kp[0]][$kp[1]][$kp[2]];
+ case 4:
+ if ( isset( $arr[$kp[0]][$kp[1]][$kp[2]][$kp[3]] ) )
+ return $arr[$kp[0]][$kp[1]][$kp[2]][$kp[3]];
+ // if there are more then 4 parts loop trough them
+ $curr = $arr;
+ foreach( $kp as $k )
+ if ( isset( $curr[$k] ) ) {
+ $curr = $curr[$k];
+ return $default;
+ return $curr;
+ * checks if the array has an item with dottet dimensions
+ public static function has( $key, $arr )
+ switch ( count( $kp ) ) {
+ return isset( $arr[$kp[0]][$kp[1]] ); break;
+ return isset( $arr[$kp[0]][$kp[1]][$kp[2]] ); break;
+ return isset( $arr[$kp[0]][$kp[1]][$kp[2]][$kp[3]] ); break;
+ * sets an item from an array with dottet dimensions
+ public static function set( $key, $value, &$arr )
+ if ( strpos( $key, '.' ) === false )
+ $arr[$key] = $value;
+ $arr[$kp[0]][$kp[1]] = $value; break;
+ $arr[$kp[0]][$kp[1]][$kp[2]] = $value; break;
+ $arr[$kp[0]][$kp[1]][$kp[2]][$kp[3]] = $value; break;
+ $kp = array_reverse( $kp );
+ $curr = $value;
+ $curr = array( $k => $curr );
+ $arr = static::merge( $arr, $curr );
+ * deletes an item from an array with dottet dimensions
+ public static function delete( $key, &$arr )
+ unset( $arr[$key] ); return;
+ unset( $arr[$kp[0]][$kp[1]] ); return; break;
+ unset( $arr[$kp[0]][$kp[1]][$kp[2]] ); return; break;
+ unset( $arr[$kp[0]][$kp[1]][$kp[2]][$kp[3]] ); return; break;
@@ -0,0 +1,302 @@
+ * Asset handler
+ * This is helper your front end stuff like js file, images etc..
+class CCAsset
+ * instance holder
+ protected static $instances = array();
+ * default instance
+ protected static $_default = 'main';
+ * The macros holder
+ protected static $_macros = array();
+ * CSS Macro - Generates a css stylesheet link
+ * @param string $uri
+ * @param string $holder
+ protected static function macro_css( $uri, $holder = null )
+ return '<link type="text/css" rel="stylesheet" href="'.static::uri( $uri, $holder ).'" />';
+ * JS Macro - Generates a js source link
+ protected static function macro_js( $uri, $holder = null )
+ return '<script type="text/javascript" src="'.static::uri( $uri, $holder ).'"></script>';
+ * LESS Macro - Generates a less stylesheet link
+ protected static function macro_less( $uri, $holder = null )
+ return '<link type="text/css" rel="stylesheet/less" href="'.static::uri( $uri, $holder ).'" />';
+ * IMG Macro - Generates a less stylesheet link
+ protected static function macro_img( $uri, $holder = null )
+ return '<img src="'.static::uri( $uri, $holder ).'" />';
+ * Default Macro - Simply returns the given string
+ protected static function macro__( $string )
+ return $string;
+ * OG Macro - Generates open grapth tags
+ * // <meta property="og:type" content="video" />
+ * CCAsset::og( 'type', 'video' );
+ * // <meta property="og:res" content="1080p" />
+ * CCAsset::og( array( 'type' => 'video', 'res' => '1080p' ));
+ * @param array|string $tags
+ * @param string $content
+ protected static function macro_og( $tags, $content = null )
+ if ( !is_array( $tags ) )
+ $tags = array( $tags => $content );
+ foreach( $tags as $key => $content )
+ $buffer .= '<meta property="og:'.$key.'" content="'.$content.'" />';
+ return $buffer;
+ * Register an assets macro
+ * @param callback|string $callback
+ static::$_macros[$key] = $callback;
+ * Check for macros and execute them
+ if ( !array_key_exists( $name, static::$_macros ) )
+ // we always check if the global class has that marco this way we
+ // can easly extend CCAsset default macros.
+ if ( !method_exists( '\\CCAsset', 'macro_'.$name ) )
+ throw new \BadMethodCallException( "CCAsset::".$name." - No method or macro found." );
+ return call_user_func_array( array( '\\CCAsset', 'macro_'.$name ), $arguments );
+ // if we have a string handle the macro as replacement pattern.
+ if ( is_string( static::$_macros[$name] ) )
+ // in this case argument 1 is going to be the uri
+ // and argument 2 the asset holder
+ list( $uri, $holder ) = $arguments;
+ return str_replace( ':uri', static::uri( $uri, $holder ), static::$_macros[$name] );
+ return call_user_func_array( static::$_macros[$name], $arguments );
+ * Get the uri to an asset
+ * // /assets/images/something.jpg
+ * CCAsset::uri( 'images/something.jpg' );
+ * // /assets/MyTheme/images/something.jpg
+ * CCAsset::uri( 'images/logo.png', 'theme' );
+ * // will not modify the given url
+ * CCAsset::uri( 'http://example.com/images/logo.jpg' );
+ public static function uri( $uri, $name = null )
+ // when the uri conains a protocol we just return
+ // the given uri.
+ if ( strpos( $uri, '://' ) === false )
+ // If the path is relative and not absolute
+ // we prefix the holder path.
+ if ( substr( $uri, 0, 1 ) != '/' )
+ $uri = CCUrl::to( static::holder( $name )->path.$uri );
+ // When the given uri is absolute we remove the first
+ // slash and let CCUrl do the job.
+ $uri = CCUrl::to( substr( $uri, 1 ) );
+ return $uri;
+ * Get an asset holder instance.
+ * $holder = CCAsset::holder();
+ * $holder = CCAsset::holder( 'footer' );
+ * @return CCAsset
+ public static function holder( $name = null )
+ if ( !isset( $name ) )
+ if ( !isset( static::$instances[$name] ) )
+ static::$instances[$name] = new CCAsset_Holder();
+ return static::$instances[$name];
+ * Get all assets of a specific type in a holder and run them
+ * trough the maching macros.
+ * CCAsset::code( 'css' );
+ * CCAsset::code( 'js', 'footer' );
+ public static function code( $type, $name = null )
+ foreach( static::holder( $name )->get( $type ) as $item )
+ $buffer .= call_user_func( array( '\\CCAsset', $type ), $item, $name );
+ * Add an asset to the holder. Basically this method checks the
+ * file extension to sort them and generate the correct code using the macros.
+ * CCAsset::add( 'jquery.js' );
+ * CCAsset::add( 'style.css' );
+ * CCAsset::add( '<script>document.write( "Hello World" );</script>', 'footer' );
+ * @param string $item
+ public static function add( $item, $name = null )
+ return static::holder( $name )->add( $item );
+ * Get all assets by a specific macro / type
+ * CCAsset::get();
+ * CCAsset::get( 'css', 'theme' );
+ * @param string $type By passing null all items from all types are returned
+ public static function get( $type = null, $name = null )
+ return static::holder( $name )->get( $type );
+ * Clear the asset holder, deletes all contained assets
+ * of a special type or if $type is null everything.
+ * CCAsset::clear();
+ * CCAsset::clear( 'css' );
+ * CCAsset::clear( 'js', 'footer' );
+ public static function clear( $type, $name = null )
+ return static::holder( $name )->clear( $type );
@@ -0,0 +1,106 @@
+ * This is the Asset holder
+class CCAsset_Holder
+ * path modifier
+ public $path = 'assets/';
+ * Content
+ public $assets = array();
+ * $holder->add( 'jquery.js' );
+ * $holder->add( 'style.css' );
+ * $holder->add( '<script>document.write( "Hello World" );</script>' );
+ public function add( $item )
+ // when the first character is a "smaller than" we simply assume
+ // that a custom tag has been passed and not a filepath
+ if ( strpos( $item, "<" ) !== false )
+ $macro = '_';
+ $macro = CCStr::extension( $item );
+ if ( !isset( $this->assets[$macro] ) )
+ $this->assets[$macro] = array();
+ $this->assets[$macro][] = $item;
+ * $holder->get();
+ * $holder->get( 'css' );
+ public function get( $type = null )
+ if ( is_null( $type ) )
+ return $this->assets;
+ if ( !isset( $this->assets[$type] ) )
+ return $this->assets[$type];
+ * $holder->clear();
+ * $holder->clear( 'js' );
+ public function clear( $type = null )
+ $this->assets = array();
+ if ( isset( $this->assets[$type] ) )
+ $this->assets[$type] = array();
@@ -0,0 +1,277 @@
+ * Command line interface
+ * Some helpers when using the command line interface.
+ * FuelPHP helped me alot with this class thanks your awesome framework guys.
+ * Go check it out: http://fuelphp.com
+class CCCli
+ * foreground colors
+ protected static $colors = array(
+ 'black' => '0;30',
+ 'dark_gray' => '1;30',
+ 'blue' => '0;34',
+ 'dark_blue' => '1;34',
+ 'light_blue' => '1;34',
+ 'green' => '0;32',
+ 'success' => '0;32',
+ 'light_green' => '1;32',
+ 'cyan' => '0;36',
+ 'info' => '0;36',
+ 'light_cyan' => '1;36',
+ 'red' => '0;31',
+ 'error' => '0;31',
+ 'light_red' => '1;31',
+ 'purple' => '0;35',
+ 'light_purple' => '1;35',
+ 'light_yellow' => '0;33',
+ 'yellow' => '1;33',
+ 'warning' => '1;33',
+ 'light_gray' => '0;37',
+ 'white' => '1;37',
+ * background colors
+ protected static $background_colors = array(
+ 'black' => '40',
+ 'red' => '41',
+ 'green' => '42',
+ 'yellow' => '43',
+ 'blue' => '44',
+ 'magenta' => '45',
+ 'cyan' => '46',
+ 'light_gray' => '47',
+ * Check if php is running on windows cmd
+ public static function is_windows()
+ return strtolower( substr( PHP_OS, 0, 3 ) ) === 'win';
+ * Write to the standard output device
+ public static function write( $str, $color = null, $background = null )
+ if ( !is_null( $color ) )
+ $str = static::color( $str, $color, $background );
+ fwrite( STDOUT, $str );
+ * Write to the standard output device with an end of line.
+ public static function line( $str, $color = null, $background = null )
+ if ( is_array( $str ) )
+ $str = implode( PHP_EOL, $str );
+ static::write( $str.PHP_EOL, $color, $background );
+ * Display a success message
+ * @param string $prefix
+ public static function success( $str, $prefix = 'success' )
+ static::line( static::color( $prefix, 'green' ).' '.$str );
+ * Display an error message
+ public static function error( $str, $prefix = 'failure' )
+ static::line( static::color( $prefix, 'red' ).' '.$str );
+ * Display a warning message
+ public static function warning( $str, $prefix = 'warning' )
+ static::line( static::color( $prefix, 'yellow' ).' '.$str );
+ * Display an info message
+ public static function info( $str, $prefix = 'info' )
+ static::line( static::color( $prefix, 'cyan' ).' '.$str );
+ * Write empty lines
+ * @param int $count
+ public static function new_line( $count = 1 )
+ for( $i=0; $i<$count; $i++ )
+ static::line( '' );
+ * Add color to string
+ * Check the available colors at the top $colors & $background_colors
+ * @param string $color
+ * @param string $background
+ public static function color( $str, $color, $background = null )
+ // ANSI escape codes are not available on Windows CMD
+ // So we return the string unmodified
+ if ( static::is_windows() )
+ return $str;
+ $out = "\033[".static::$colors[$color]."m";
+ if ( !is_null( $background ) )
+ $out .= "\033[".static::$background_colors[$background]."m";
+ return $out.$str."\033[0m";
+ * Clear the screen
+ public static function clear()
+ static::new_line(40); return;
+ static::write( chr(27)."[H".chr(27)."[2J" );
+ * Get a bool value from the user
+ * This will write the question down and waiting for a yes or no.
+ * When strict is true, it wil only accept yes or no. And not y, n, jep etc.
+ * @param string $question
+ * @param bool $strict
+ public static function confirm( $question, $strict = false )
+ $question .= ' [yes/no]: ';
+ if ( $strict )
+ $res = strtolower( static::read( $question ) );
+ while ( $res !== 'yes' && $res !== 'no' );
+ return ( $res == 'yes' ) ? true : false;
+ while ( empty( $res ) );
+ $positives = array(
+ 'yes', 'y', 'ya', 'ye', 'yeah', 'yup',
+ 'jep', 'jap', 'ja',
+ 'si', 'sim',
+ 'true',
+ 'hai',
+ 'oui',
+ 'no problemo',
+ return in_array( $res, $positives );
+ * Read from comman line
+ * Because windows does not support readline we use normal fgets in that case.
+ * @param string $prefix The prefix will be printet before the user can respond.
+ public static function read( $prefix = '' )
+ if ( !static::is_windows() )
+ $line = readline( $prefix );
+ readline_add_history( $line );
+ static::write( $prefix );
+ $line = trim( fgets( STDIN ) );
@@ -0,0 +1,69 @@
+ * ClanCats color helper
+ * @package ClanCats-Framework
+ * @version 0.5
+ * @copyright 2010 - 2013 ClanCats GmbH
+class CCColor
+ * parse an color
+ * @param mixed $color
+ public static function create( $color )
+ // our return
+ $rgb = array();
+ if ( is_array( $color ) )
+ $color = array_values( $color );
+ $rgb[0] = $color[0];
+ $rgb[1] = $color[1];
+ $rgb[2] = $color[2];
+ // parse hex color
+ elseif ( is_string( $color ) && substr( $color, 0, 1 ) == '#' )
+ $color = substr( $color, 1 );
+ if( strlen( $color ) == 3 )
+ $rgb[0] = hexdec( substr( $color, 0, 1 ) . substr( $color, 0, 1 ) );
+ $rgb[1] = hexdec( substr( $color, 1, 1 ) . substr( $color, 1, 1 ) );
+ $rgb[2] = hexdec( substr( $color, 2, 1 ) . substr( $color, 2, 1 ) );
+ elseif( strlen( $color ) == 6 )
+ $rgb[0] = hexdec( substr( $color, 0, 2 ) );
+ $rgb[1] = hexdec( substr( $color, 2, 2 ) );
+ $rgb[2] = hexdec( substr( $color, 4, 2 ) );
+ // could not be parsed
+ return new static( $rgb );
+ * our color holder
+ public $RGB = array( 0, 0, 0 );
+ * init a new color instance
+ public function __construct( $rgb )
+ $this->RGB = $rgb;
@@ -0,0 +1,157 @@
+ * Configuration handler
+ * load, access and create configuration files
+class CCConfig extends CCDataObject
+ * Create a configuration instance
+ * @return CCConfig
+ public static function create( $name = null, $driver = 'file' )
+ return new static( null, $driver );
+ static::$instances[$name] = new static( $name, $driver );
+ * current instance name
+ protected $_instance_name = null;
+ * @param string $name The instance name used for writing
+ * @param string $driver
+ public function __construct( $name = null, $driver = 'file' )
+ $this->_instance_name = $name;
+ $this->driver( $driver );
+ if ( !is_null( $name ) )
+ $this->read( $name );
+ * Set the configuration dirver
+ public function driver( $driver )
+ $driver = CCCORE_NAMESPACE.'\\'.'CCConfig_'.ucfirst( $driver );
+ if ( !class_exists( $driver ) )
+ throw new \InvalidArgumentException("CCConfig - Invalid driver '".$driver."'");
+ * Name getter and setter
+ public function name( $name = null )
+ return $this->_instance_name;
+ * Load a configuration file
+ * This will load a configuration file and assign the data
+ public function read( $name )
+ $this->_data = $this->_driver->read( $name );
+ * save a configuration file
+ * this method overwrites your configuration file!!
+ public function write( $driver = null )
+ if ( empty( $this->_instance_name ) )
+ throw new CCException("CCConfig::write - configuration name is missing.");
+ // set the dirver
+ if ( !is_null( $driver ) )
+ // run write
+ $this->_driver->write( $this->_instance_name, $this->_data );
+ * Delete the entire configuration
+ * Attention with this one he can be evil!
+ public function _delete()
+ return $this->_driver->delete( $this->_instance_name );
@@ -0,0 +1,59 @@
+ * Configuration Driver
+class CCConfig_Array implements CCConfig_Driver
+ * Data holder
+ protected static $data = array();
+ * Read the configuration data
+ * Get the configuration data and return them as array
+ if ( array_key_exists( $name, static::$data ) )
+ return static::$data[$name];
+ * Write the configuration data
+ public function write( $name, $data )
+ static::$data[$name] = $data;
+ * delete the configuration data
+ public function delete( $name )
+ unset( static::$data[$name] );