diff --git a/.gitignore b/.gitignore deleted file mode 100644 index d6ab546..0000000 --- a/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -vendor -composer.phar -composer.lock -.DS_Store -*.sublime-* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 12ed64a..0000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: php - -branches: - only: - - master - -php: - - 5.3 - - 5.4 - -before_script: - - curl -s http://getcomposer.org/installer | php - - php composer.phar install --dev - -script: phpunit \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index b962ff0..0000000 --- a/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Feather - -Feather is a lightweight, fully awesome forum built on the Laravel framework. - -[![Build Status](https://secure.travis-ci.org/featherforums/feather.png?branch=master)](https://travis-ci.org/featherforums/feather) \ No newline at end of file diff --git a/app/config/app.php b/app/config/app.php deleted file mode 100644 index fdc52b6..0000000 --- a/app/config/app.php +++ /dev/null @@ -1,158 +0,0 @@ - true, - - /* - |-------------------------------------------------------------------------- - | Application Timezone - |-------------------------------------------------------------------------- - | - | Here you may specify the default timezone for your application, which - | will be used by the PHP date and date-time functions. We have gone - | ahead and set this to a sensible default for you out of the box. - | - */ - - 'timezone' => 'UTC', - - /* - |-------------------------------------------------------------------------- - | Application Locale Configuration - |-------------------------------------------------------------------------- - | - | The application locale determines the default locale that will be used - | by the translation service provider. You are free to set this value - | to any of the locales which will be supported by the application. - | - */ - - 'locale' => 'en', - - /* - |-------------------------------------------------------------------------- - | Application Fallback Locale - |-------------------------------------------------------------------------- - | - | The fallback locale determines the locale to use when the current one - | is not available. You may change the value to correspond to any of - | the language folders that are provided through your application. - | - */ - - 'fallback_locale' => 'en', - - /* - |-------------------------------------------------------------------------- - | Encryption Key - |-------------------------------------------------------------------------- - | - | This key is used by the Illuminate encrypter service and should be set - | to a random, long string, otherwise these encrypted values will not - | be safe. Make sure to change it before deploying any application! - | - */ - - 'key' => 'YourSecretKey!!!', - - /* - |-------------------------------------------------------------------------- - | Autoloaded Service Providers - |-------------------------------------------------------------------------- - | - | The service providers listed here will be automatically loaded on the - | request to your application. Feel free to add your own services to - | this array to grant expanded functionality to your applications. - | - */ - - 'providers' => array( - - 'Illuminate\Foundation\Providers\AliasServiceProvider', - 'Illuminate\Foundation\Providers\ArtisanServiceProvider', - 'Illuminate\Auth\AuthServiceProvider', - 'Illuminate\Cache\CacheServiceProvider', - 'Illuminate\Foundation\Providers\ComposerServiceProvider', - 'Illuminate\Routing\ControllerServiceProvider', - 'Illuminate\CookieServiceProvider', - 'Illuminate\Database\DatabaseServiceProvider', - 'Illuminate\EncryptionServiceProvider', - 'Illuminate\Events\EventServiceProvider', - 'Illuminate\FilesystemServiceProvider', - 'Illuminate\Hashing\HashServiceProvider', - 'Illuminate\Log\LogServiceProvider', - 'Illuminate\Mail\MailServiceProvider', - 'Illuminate\Database\MigrationServiceProvider', - 'Illuminate\Pagination\PaginationServiceProvider', - 'Illuminate\Foundation\Providers\PublisherServiceProvider', - 'Illuminate\Redis\RedisServiceProvider', - 'Illuminate\Database\SeedServiceProvider', - 'Illuminate\Session\SessionServiceProvider', - 'Illuminate\Translation\TranslationServiceProvider', - 'Illuminate\Validation\ValidationServiceProvider', - 'Illuminate\View\ViewServiceProvider', - 'Basset\BassetServiceProvider', - - ), - - /* - |-------------------------------------------------------------------------- - | Class Aliases - |-------------------------------------------------------------------------- - | - | This array of class aliases will be registered when this application - | is started. However, feel free to register as many as you wish as - | the aliases are "lazy" loaded so they don't hinder performance. - | - */ - - 'aliases' => array( - - 'App' => 'Illuminate\Foundation\Facades\App', - 'Artisan' => 'Illuminate\Foundation\Facades\Artisan', - 'Auth' => 'Illuminate\Foundation\Facades\Auth', - 'Basset' => 'Basset\Facades\Basset', - 'Cache' => 'Illuminate\Foundation\Facades\Cache', - 'Config' => 'Illuminate\Foundation\Facades\Config', - 'Controller' => 'Illuminate\Routing\Controllers\Controller', - 'Cookie' => 'Illuminate\Foundation\Facades\Cookie', - 'Crypt' => 'Illuminate\Foundation\Facades\Crypt', - 'Crypt' => 'Illuminate\Foundation\Facades\Crypt', - 'DB' => 'Illuminate\Foundation\Facades\DB', - 'Eloquent' => 'Illuminate\Database\Eloquent\Model', - 'Event' => 'Illuminate\Foundation\Facades\Event', - 'Feather\Extension' => 'Feather\Facades\Extension', - 'Feather\Presenter' => 'Feather\Facades\Presenter', - 'File' => 'Illuminate\Foundation\Facades\File', - 'Hash' => 'Illuminate\Foundation\Facades\Hash', - 'Input' => 'Illuminate\Foundation\Facades\Input', - 'Lang' => 'Illuminate\Foundation\Facades\Lang', - 'Log' => 'Illuminate\Foundation\Facades\Log', - 'Mail' => 'Illuminate\Foundation\Facades\Mail', - 'Paginator' => 'Illuminate\Foundation\Facades\Paginator', - 'Redirect' => 'Illuminate\Foundation\Facades\Redirect', - 'Redis' => 'Illuminate\Foundation\Facades\Redis', - 'Request' => 'Illuminate\Foundation\Facades\Request', - 'Response' => 'Illuminate\Foundation\Facades\Response', - 'Route' => 'Illuminate\Foundation\Facades\Route', - 'Schema' => 'Illuminate\Foundation\Facades\Schema', - 'Session' => 'Illuminate\Foundation\Facades\Session', - 'URL' => 'Illuminate\Foundation\Facades\URL', - 'Validator' => 'Illuminate\Foundation\Facades\Validator', - 'View' => 'Illuminate\Foundation\Facades\View', - - ), - -); diff --git a/app/config/auth.php b/app/config/auth.php deleted file mode 100644 index 3eceea3..0000000 --- a/app/config/auth.php +++ /dev/null @@ -1,46 +0,0 @@ - 'eloquent', - - /* - |-------------------------------------------------------------------------- - | Authentication Model - |-------------------------------------------------------------------------- - | - | When using the "Eloquent" authentication driver, we need to know which - | Eloquent model should be used to retrieve your users. Of course, it - | is often just the "User" model but you may use whatever you like. - | - */ - - 'model' => 'User', - - /* - |-------------------------------------------------------------------------- - | Authentication Table - |-------------------------------------------------------------------------- - | - | When using the "Database" authentication driver, we need to know which - | table should be used to retrieve your users. We have chosen a basic - | default value but you may easily change it to any table you like. - | - */ - - 'table' => 'users', - -); diff --git a/app/config/cache.php b/app/config/cache.php deleted file mode 100644 index eb47792..0000000 --- a/app/config/cache.php +++ /dev/null @@ -1,89 +0,0 @@ - 'file', - - /* - |-------------------------------------------------------------------------- - | File Cache Location - |-------------------------------------------------------------------------- - | - | When using the "file" cache driver, we need a location where the cache - | files may be stored. A sensible default has been specified, but you - | are free to change it to any other place on disk that you desire. - | - */ - - 'path' => __DIR__.'/../storage/cache', - - /* - |-------------------------------------------------------------------------- - | Database Cache Connection - |-------------------------------------------------------------------------- - | - | When using the "database" cache driver you may specify the connection - | that should be used to store the cached items. When this option is - | null the default database connection will be utilized for cache. - | - */ - - 'connection' => null, - - /* - |-------------------------------------------------------------------------- - | Database Cache Table - |-------------------------------------------------------------------------- - | - | When using the "database" cache driver we need to know the table that - | should be used to store the cached items. A default table name has - | been provided but you're free to change it however you deem fit. - | - */ - - 'table' => 'cache', - - /* - |-------------------------------------------------------------------------- - | Memcached Servers - |-------------------------------------------------------------------------- - | - | Now you may specify an array of your Memcached servers that should be - | used when utilizing the Memcached cache driver. All of the servers - | should contain a value for "host", "port", and "weight" options. - | - */ - - 'memcached' => array( - - array('host' => '127.0.0.1', 'port' => 11211, 'weight' => 100), - - ), - - /* - |-------------------------------------------------------------------------- - | Cache Key Prefix - |-------------------------------------------------------------------------- - | - | When utilizing a RAM based store such as APC or Memcached, there might - | be other applications utilizing the asme cache. So, we'll specify a - | value to get prefixed to all our keys so we can avoid collisions. - | - */ - - 'prefix' => 'laravel', - -); diff --git a/app/config/database.php b/app/config/database.php deleted file mode 100644 index 4b93333..0000000 --- a/app/config/database.php +++ /dev/null @@ -1,159 +0,0 @@ - PDO::FETCH_CLASS, - - /* - |-------------------------------------------------------------------------- - | Default Database Connection Name - |-------------------------------------------------------------------------- - | - | Here you may specify which of the database connections below you wish - | to use as your default connection for all database work. Of course - | you may use many connections at once using the Database library. - | - */ - - 'default' => 'mysql', - - /* - |-------------------------------------------------------------------------- - | Database Connections - |-------------------------------------------------------------------------- - | - | Here are each of the database connections setup for your application. - | Of course, examples of configuring each database platform that is - | supported by Laravel is shown below to make development simple. - | - | - | All database work in Laravel is done through the PHP PDO facilities - | so make sure you have the driver for your particular database of - | choice installed on your machine before you begin development. - | - */ - - 'connections' => array( - - 'sqlite' => array( - 'driver' => 'sqlite', - 'database' => __DIR__.'/../database/production.sqlite', - 'prefix' => '', - ), - - 'mysql' => array( - 'driver' => 'mysql', - 'host' => 'localhost', - 'database' => 'feather', - 'username' => 'root', - 'password' => '', - 'charset' => 'utf8', - 'collation' => 'utf8_general_ci', - 'prefix' => '', - ), - - 'pgsql' => array( - 'driver' => 'pgsql', - 'host' => 'localhost', - 'database' => 'database', - 'username' => 'root', - 'password' => '', - 'charset' => 'utf8', - 'prefix' => '', - 'schema' => 'public', - ), - - 'sqlsrv' => array( - 'driver' => 'sqlsrv', - 'host' => 'localhost', - 'database' => 'database', - 'username' => 'root', - 'password' => '', - 'prefix' => '', - ), - - ), - - /* - |-------------------------------------------------------------------------- - | Database Migration Options - |-------------------------------------------------------------------------- - | - | Database migrations are version control for your database and are very - | nice when you're working in a team environment. Here you will set a - | few of the migration options, like the migrations directory path. - | - */ - - 'migration' => array( - - /* - |-------------------------------------------------------------------------- - | Migration Paths - |-------------------------------------------------------------------------- - | - | This array contains the paths to your migration files. For almost every - | appliactions you build, the default "application" path that is given - | will suffice; however you're free to include more migration paths. - | - | - | Once a new path is added you may use the "--package" switch the console - | application's "migrate:make" command to create a new migrations file - | that is located at that location without specifying the full path. - | - */ - - 'paths' => array( - - 'application' => __DIR__.'/../database/migrations', - - ), - - /* - |-------------------------------------------------------------------------- - | Migration Repository Table - |-------------------------------------------------------------------------- - | - | This table keeps track of all the migrations that have already run for - | your application. Using this information, we can determine which of - | the migrations on disk have not actually be run in the databases. - | - */ - - 'table' => 'migrations', - - ), - - /* - |-------------------------------------------------------------------------- - | Redis Databases - |-------------------------------------------------------------------------- - | - | Redis is an open source, fast, and advanced key-value store that also - | provides a richer set of commands than a typical key-value systems - | such as APC or Memcached. Laravel makes it easy to dig right in. - | - */ - - 'redis' => array( - - 'default' => array( - 'host' => '127.0.0.1', - 'port' => 6379, - 'database' => 0, - ), - - ), - -); \ No newline at end of file diff --git a/app/config/mail.php b/app/config/mail.php deleted file mode 100644 index e793d88..0000000 --- a/app/config/mail.php +++ /dev/null @@ -1,83 +0,0 @@ - 'smtp.postmarkapp.com', - - /* - |-------------------------------------------------------------------------- - | SMTP Host Port - |-------------------------------------------------------------------------- - | - | This is the SMTP port used by your application to delivery e-mails to - | users of your application. Like the host we have set this value to - | stay compatible with the Postmark e-mail application by default. - | - */ - - 'port' => 2525, - - /* - |-------------------------------------------------------------------------- - | Global "From" Address - |-------------------------------------------------------------------------- - | - | You may wish for all e-mails sent by your application to be sent from - | the same address. Here, you may specify a name and address that is - | used globally for all e-mails that are sent by your application. - | - */ - - 'from' => array('address' => null, 'name' => null), - - /* - |-------------------------------------------------------------------------- - | E-Mail Encryption Protocol - |-------------------------------------------------------------------------- - | - | Here you may specify the encryption protocol that should be used when - | the application send e-mail messages. A sensible default using the - | transport layer security protocol should provide great security. - | - */ - - 'encryption' => 'tls', - - /* - |-------------------------------------------------------------------------- - | SMTP Server Username - |-------------------------------------------------------------------------- - | - | If your SMTP server requires a username for authentication, you should - | set it here. This will get used to authenticate with your server on - | connection. You may also set the "password" value below this one. - | - */ - - 'username' => null, - - /* - |-------------------------------------------------------------------------- - | SMTP Server Password - |-------------------------------------------------------------------------- - | - | Here you may set the password required by your SMTP server to send out - | messages from your application. This will be given to the server on - | connection so that the application will be able to send messages. - | - */ - - 'password' => null, - -); diff --git a/app/config/packages/.gitkeep b/app/config/packages/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/config/packages/jasonlewis/basset.php b/app/config/packages/jasonlewis/basset.php deleted file mode 100644 index ef90021..0000000 --- a/app/config/packages/jasonlewis/basset.php +++ /dev/null @@ -1,122 +0,0 @@ - 'assets', - - /* - |-------------------------------------------------------------------------- - | Asset Directories - |-------------------------------------------------------------------------- - | - | These named directories are used for quick reference as well as when - | searching for an asset. Assets are located by cascading through the array - | of directories until an asset with the matching name is found. - | - | Directories are relative from the root of your application. - | - | You can specifiy an absolute path to a directory by prefixing it with - | 'path: '. - | - | array( - | 'css' => 'path: /path/to/your/directory' - | ) - | - */ - - 'directories' => array(), - - /* - |-------------------------------------------------------------------------- - | Asset Collections - |-------------------------------------------------------------------------- - | - | Define your collections in an array like so. - | - | array( - | 'website' => function($collection) - | { - | $collection->add('example.css'); - | } - | ) - | - | This collection is now available at Basset::show('website.css') - | - */ - 'collections' => array(), - - /* - |-------------------------------------------------------------------------- - | Compiling Path - |-------------------------------------------------------------------------- - | - | When assets are statically compiled via the command line the generated - | files will be stored in this directory. The path is relative to the root - | of your application, so remember to compile them to the public directory - | somewhere. - | - | If the directory does not exist, Basset will attempt to create it. - | - */ - - 'compiling_path' => 'public/assets', - - /* - |-------------------------------------------------------------------------- - | Production Environment - |-------------------------------------------------------------------------- - | - | Basset will attempt to detect your production environment and serve - | static assets. You can help Basset out in a number of ways to speed it up - | a bit. - | - | Set your actual production environment here and Basset will compare - | environments and serve the appropriate assets. - | - | Set to null or an empty string and Basset will try and detect your - | environment, this may deliver unexpected results. - | - | Set to false and Basset will always serve individual assets as it does in - | a development environment. Remember that filters will not be applied - | to the assets. - | - | Set to true to always serve static assets if available. - | - */ - - 'production_environment' => '', - - /* - |-------------------------------------------------------------------------- - | Named Filters - |-------------------------------------------------------------------------- - | - | A named filter can be used to quickly apply a filter to a collection of - | assets. - | - | 'YuiCss' => 'Yui\CssCompressorFilter' - | - | If you'd like to specify options for a named filter you can define the - | filter as an array. - | - | 'YuiCss' => array( - | 'Yui\CssCompressorFilter' => array('/path/to/yuicompressor.jar') - | ) - | - | The filter can then be referenced by its name when applying filters. - | - */ - - 'filters' => array() - -); \ No newline at end of file diff --git a/app/config/session.php b/app/config/session.php deleted file mode 100644 index 188c8c7..0000000 --- a/app/config/session.php +++ /dev/null @@ -1,86 +0,0 @@ - 'cookie', - - /* - |-------------------------------------------------------------------------- - | Session Lifetime - |-------------------------------------------------------------------------- - | - | Here you may specify the number of minutes that you wish the session - | to be allowed to remain idle for it is expired. If you want them - | to immediately expire when the browser closes, set it to zero. - | - */ - - 'lifetime' => 120, - - /* - |-------------------------------------------------------------------------- - | Session File Location - |-------------------------------------------------------------------------- - | - | When using the "file" session driver, we need a location where session - | files may be stored. A default has been set for you but a different - | location may be specified. This is only needed for file sessions. - | - */ - - 'path' => __DIR__.'/../storage/sessions', - - /* - |-------------------------------------------------------------------------- - | Session Database Connection - |-------------------------------------------------------------------------- - | - | When using the "database" session driver, you may specify the database - | connection that should be used to manage your sessions. This should - | correspond to a connection in your "database" configuration file. - | - */ - - 'connection' => null, - - /* - |-------------------------------------------------------------------------- - | Session Database Table - |-------------------------------------------------------------------------- - | - | When using the "database" session driver, you may specify the table we - | should use to manage the sessions. Of course, a sensible default is - | provided for you; however, you are free to change this as needed. - | - */ - - 'table' => 'sessions', - - /* - |-------------------------------------------------------------------------- - | Session Sweeping Lottery - |-------------------------------------------------------------------------- - | - | Some session drivers must manually sweep their storage location to get - | rid of old sessions from storage. Here are the chances that it will - | happen on a given request. By default, the odds are 2 out of 100. - | - */ - - 'lottery' => array(2, 100), - -); diff --git a/app/config/testing/cache.php b/app/config/testing/cache.php deleted file mode 100644 index 16d3ae2..0000000 --- a/app/config/testing/cache.php +++ /dev/null @@ -1,20 +0,0 @@ - 'array', - -); \ No newline at end of file diff --git a/app/config/testing/session.php b/app/config/testing/session.php deleted file mode 100644 index 338aeba..0000000 --- a/app/config/testing/session.php +++ /dev/null @@ -1,21 +0,0 @@ - 'array', - -); \ No newline at end of file diff --git a/app/config/view.php b/app/config/view.php deleted file mode 100644 index eba10a4..0000000 --- a/app/config/view.php +++ /dev/null @@ -1,31 +0,0 @@ - array(__DIR__.'/../views'), - - /* - |-------------------------------------------------------------------------- - | Pagination View - |-------------------------------------------------------------------------- - | - | This view will be used to render the pagination link output, and can - | be easily customized here to show any view you like. A clean view - | compatible with Twitter's Bootstrap is given to you by default. - | - */ - - 'pagination' => 'pagination::slider', - -); diff --git a/app/controllers/.gitkeep b/app/controllers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/controllers/BaseController.php b/app/controllers/BaseController.php deleted file mode 100644 index 817e208..0000000 --- a/app/controllers/BaseController.php +++ /dev/null @@ -1,56 +0,0 @@ -app = Facade::getFacadeApplication(); - - if ($this->layout) - { - $this->layout = $this->app['view']->make($this->layout); - } - } - - /** - * Call the given action with the given parameters. - * - * @param string $method - * @param array $parameters - * @return mixed - */ - protected function directCallAction($method, $parameters) - { - // If no response is returned by the controller method then we'll fall back to - // the layout that was bound in the constructor. - if ( ! $response = call_user_func_array(array($this, $method), $parameters)) - { - $response = $this->layout; - } - - return $response; - } - -} \ No newline at end of file diff --git a/app/controllers/HomeController.php b/app/controllers/HomeController.php deleted file mode 100644 index 8ff715b..0000000 --- a/app/controllers/HomeController.php +++ /dev/null @@ -1,12 +0,0 @@ - '« Previous', - 'next' => 'Next »', - -); \ No newline at end of file diff --git a/app/lang/en/validation.php b/app/lang/en/validation.php deleted file mode 100644 index 7ee461c..0000000 --- a/app/lang/en/validation.php +++ /dev/null @@ -1,86 +0,0 @@ - "The :attribute must be accepted.", - "activeurl" => "The :attribute is not a valid URL.", - "after" => "The :attribute must be a date after :date.", - "alpha" => "The :attribute may only contain letters.", - "alphadash" => "The :attribute may only contain letters, numbers, and dashes.", - "alphanum" => "The :attribute may only contain letters and numbers.", - "before" => "The :attribute must be a date before :date.", - "between" => array( - "numeric" => "The :attribute must be between :min - :max.", - "file" => "The :attribute must be between :min - :max kilobytes.", - "string" => "The :attribute must be between :min - :max characters.", - ), - "confirmed" => "The :attribute confirmation does not match.", - "different" => "The :attribute and :other must be different.", - "email" => "The :attribute format is invalid.", - "exists" => "The selected :attribute is invalid.", - "image" => "The :attribute must be an image.", - "in" => "The selected :attribute is invalid.", - "integer" => "The :attribute must be an integer.", - "ip" => "The :attribute must be a valid IP address.", - "match" => "The :attribute format is invalid.", - "max" => array( - "numeric" => "The :attribute must be less than :max.", - "file" => "The :attribute must be less than :max kilobytes.", - "string" => "The :attribute must be less than :max characters.", - ), - "mimes" => "The :attribute must be a file of type: :values.", - "min" => array( - "numeric" => "The :attribute must be at least :min.", - "file" => "The :attribute must be at least :min kilobytes.", - "string" => "The :attribute must be at least :min characters.", - ), - "notin" => "The selected :attribute is invalid.", - "numeric" => "The :attribute must be a number.", - "required" => "The :attribute field is required.", - "same" => "The :attribute and :other must match.", - "size" => array( - "numeric" => "The :attribute must be :size.", - "file" => "The :attribute must be :size kilobytes.", - "string" => "The :attribute must be :size characters.", - ), - "unique" => "The :attribute has already been taken.", - "url" => "The :attribute format is invalid.", - - /* - |-------------------------------------------------------------------------- - | Custom Validation Language Lines - |-------------------------------------------------------------------------- - | - | Here you may specify custom validation messages for attributes using the - | convention "attribute.rule" to name the lines. This makes it quick to - | specify a specific custom language line for a given attribute rule. - | - */ - - 'custom' => array(), - - /* - |-------------------------------------------------------------------------- - | Custom Validation Attributes - |-------------------------------------------------------------------------- - | - | The following language lines are used to swap attribute place-holders - | with something more reader friendly such as E-Mail Address instead - | of "email". This simply helps us make messages a little cleaner. - | - */ - - 'attributes' => array(), - -); \ No newline at end of file diff --git a/app/models/BaseModel.php b/app/models/BaseModel.php deleted file mode 100644 index ac69248..0000000 --- a/app/models/BaseModel.php +++ /dev/null @@ -1,65 +0,0 @@ - array('place', 'place_id'), - 'user' => array('user', 'user_id'), - 'author' => array('user', 'user_id') - ); - - /** - * Get a new query builder for the model's table. - * - * @return Feather\Models\Builder - */ - public function newQuery() - { - $builder = new EloquentBuilder($this->newBaseQueryBuilder()); - - // Once we have the query builders, we will set the model instances so the - // builder can easily access any information it may need from the model - // while it is constructing and executing various queries against it. - $builder->setModel($this); - - return $builder; - } - - /** - * Handle the dynamic retrieval of attributes and associations. - * - * @param string $key - * @return mixed - */ - public function __get($key) - { - if (isset($this->cachable[$key]) and ! isset($this->attributes[$key]) and ! isset($this->relations[$key])) - { - list($group, $foreign) = $this->cachable[$key]; - - if (Cache::has("{$group}_{$this->$foreign}")) - { - return Cache::get("{$group}_{$this->$foreign}"); - } - } - - return parent::__get($key); - } - -} \ No newline at end of file diff --git a/app/models/Config.php b/app/models/Config.php deleted file mode 100644 index 18b149e..0000000 --- a/app/models/Config.php +++ /dev/null @@ -1,34 +0,0 @@ -get(); - }); - } - -} \ No newline at end of file diff --git a/app/models/Migrator.php b/app/models/Migrator.php deleted file mode 100644 index 2e9acb7..0000000 --- a/app/models/Migrator.php +++ /dev/null @@ -1,35 +0,0 @@ -where('driver', $driver)->first(); - } - - public function getOptions($options) - { - return (object) json_decode($options); - } - -} \ No newline at end of file diff --git a/app/models/User/Roles.php b/app/models/User/Roles.php deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes.php b/app/routes.php deleted file mode 100644 index 1e1cf7e..0000000 --- a/app/routes.php +++ /dev/null @@ -1,14 +0,0 @@ -resolveCommands(array( - 'command.feather', - 'command.feather.publish', - 'command.feather.install' -)); \ No newline at end of file diff --git a/app/start/console.php b/app/start/console.php deleted file mode 100644 index 278d73d..0000000 --- a/app/start/console.php +++ /dev/null @@ -1,45 +0,0 @@ -share(function($app) -{ - return new Console\FeatherCommand; -}); - -/* -|-------------------------------------------------------------------------- -| Publish Command -|-------------------------------------------------------------------------- -| -| The publish command is responsible for publishing assets in both themes -| and extensions. -| -*/ - -$app['command.feather.publish'] = $app->share(function($app) -{ - return new Console\PublishCommand($app['asset.publisher'], $app['path.themes'], $app['path.extensions']); -}); - -/* -|-------------------------------------------------------------------------- -| Install Command -|-------------------------------------------------------------------------- -| -| The install command is responsible for installing Feather in the current -| location. -| -*/ - -$app['command.feather.install'] = $app->share(function($app) -{ - return new Console\InstallCommand; -}); \ No newline at end of file diff --git a/app/start/facades.php b/app/start/facades.php deleted file mode 100644 index 09b5626..0000000 --- a/app/start/facades.php +++ /dev/null @@ -1,25 +0,0 @@ -runningInConsole()) -{ - if ($app['cache']->has('config')) - { - $app['cache']->forget('config'); - } - - $config = new Models\Config; - - foreach ($config->everything() as $item) - { - $app['config']->set("feather.{$item->name}", $item->value); - } -} - -/* -|-------------------------------------------------------------------------- -| Feather Providers -|-------------------------------------------------------------------------- -| -| Now that most of Feather has been bootstrapped we can register the other -| providers that Feather uses. This keeps everything neat and tidy. -| -*/ - -$providers = array( - 'Feather\Presenter\PresenterServiceProvider', - 'Feather\Extensions\ExtensionServiceProvider' -); - -foreach ($providers as $provider) -{ - $app->register(new $provider($app)); -} - -/* -|-------------------------------------------------------------------------- -| Require The Facades File -|-------------------------------------------------------------------------- -| -| We'll now register some facades to easily access some of Feather's -| components statically. -| -*/ - -require_once __DIR__.'/facades.php'; - -/* -|-------------------------------------------------------------------------- -| Require The Console File -|-------------------------------------------------------------------------- -| -| Commands are registered with the application container and when Artisan -| is run they are resolved from the Artisan start file. -| -*/ - -require __DIR__.'/console.php'; - -/* -|-------------------------------------------------------------------------- -| Prepare Feather Presenter -|-------------------------------------------------------------------------- -| -| Presenter is responsible for Feather's views. We'll prepare the presenter -| now, which will start the theme and set some view paths. -| -*/ - -$app['feather.presenter']->prepare(); - -/* -|-------------------------------------------------------------------------- -| Feather Extensions -|-------------------------------------------------------------------------- -| -| Register the activated extensions with Feather. -| -*/ - -if ( ! $app->runningInConsole()) -{ - $extensions = new Models\Extension; - - $app['feather.extensions']->registerExtensions($extensions->getActivated()->toArray()); -} - -/* -|-------------------------------------------------------------------------- -| Require The Filters File -|-------------------------------------------------------------------------- -| -| Next we will load the filters file for the application. This gives us -| a nice separate location to store our route and application filter -| definitions instead of putting them all in the main routes file. -| -*/ - -require __DIR__.'/../filters.php'; \ No newline at end of file diff --git a/app/storage/.gitignore b/app/storage/.gitignore deleted file mode 100644 index 35b719c..0000000 --- a/app/storage/.gitignore +++ /dev/null @@ -1 +0,0 @@ -services.manifest \ No newline at end of file diff --git a/app/storage/cache/.gitignore b/app/storage/cache/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/app/storage/cache/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/app/storage/logs/.gitignore b/app/storage/logs/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/app/storage/logs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/app/storage/meta/.gitignore b/app/storage/meta/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/app/storage/meta/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/app/storage/sessions/.gitignore b/app/storage/sessions/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/app/storage/sessions/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/app/storage/views/.gitignore b/app/storage/views/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/app/storage/views/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/app/themes/basic/public/css/theme.css b/app/themes/basic/public/css/theme.css deleted file mode 100644 index f4daf7d..0000000 --- a/app/themes/basic/public/css/theme.css +++ /dev/null @@ -1,423 +0,0 @@ -body { - background-image: url('../img/background.png'); - padding: 0; - margin: 0; - font-family: "Helvetica", "Helvetica Neue", Arial, sans-serif; - font-size: 13px; - color: #454545; - line-height: 1.45em; -} - -input, button, textarea { - color: #454545; - font-family: "Helvetica", "Helvetica Neue", Arial, sans-serif; - font-size: 1em; -} - -ul { - list-style-type: none; -} - -a { - color: #d7634c; -} - -a:hover { - text-decoration: none; -} - -.group:after, -fieldset dd:after, -.alert-floated:after { - content: ""; - display: table; - clear: both; -} - -/** - * The container element holds all other elements and is centered on our screen. - */ -.container { - position: relative; - width: 1140px; - margin: 0 auto; - padding: 0; -} - -/** - * The header contains the logo and some vital navigation for getting around the forum. - * Our user related data will also appear here when they are signed in. - */ -.header > .logo { - clear: right; - float: left; -} - -/** - * The user section of the header is above everything and is easily to see, it's fixed. - */ -.header > .user { - float: right; - margin: 10px 0 30px; - font-size: 14px; -} - -.header > .user a { - text-decoration: none; -} - -.header > .user > ul { - list-style-type: none; - margin: 0; - padding: 0; -} - -.header > .user > ul > li { - float: left; - padding: 0 18px; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; - background-color: #fff; - margin: 0 0 0 6px; - position: relative; - line-height: 34px; -} - -.header > .user > ul > li.light { background-color: #d1c9c1; } -.header > .user > ul > li.light:hover { background-color: #d9d2cb; } - -.header > .user > ul > li.dark { background: rgba(0, 0, 0, 0.6); } -.header > .user > ul > li.dark:hover { background: rgba(0, 0, 0, 0.7); } - -.header > .user > ul > li.attn { background: rgba(215, 99, 76, 0.8); } -.header > .user > ul > li.attn:hover { background: rgba(215, 99, 76, 0.95); } - -.header > .user > ul > li.link > a { - display: block; - margin: 0 -18px; - padding: 0 18px; -} - -.header > .user > ul > li.dark a, -.header > .user > ul > li.attn a { - color: #fff; - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.15); -} - -/** - * When a menu with options is hovered we show another dropdown menu. - */ -.header > .user > ul > li.has-options:hover { - -moz-border-radius: 3px 3px 0 0; - -webkit-border-radius: 3px 3px 0 0; - border-radius: 3px 3px 0 0; -} - -.header > .user > ul > li.has-options > ul { - display: none; - list-style-type: none; - margin: 0; - padding: 0; - position: absolute; - top: 40px; - left: 0; - line-height: normal; - width: 350px; - -moz-border-radius: 0 2px 2px 2px; - -webkit-border-radius: 0 2px 2px 2px; - border-radius: 0 2px 2px 2px; - overflow: hidden; -} - -.header > .user > ul > li.dark.has-options > ul { background: rgba(0, 0, 0, 0.7); } -.header > .user > ul > li.attn.has-options > ul { background: rgba(215, 99, 76, 0.95); } - -.header > .user > ul > li.has-options:hover > ul { - display: block; -} - -/** - * Place a smaller bar after the hovered option so we can shift the actual dropdown below - * other menu items. - */ -.header > .user > ul > li.has-options:hover:before { - content: ""; - display: block; - width: 100%; - height: 6px; - position: absolute; - top: 34px; - left: 0; -} - -.header > .user > ul > li.dark.has-options:hover:before { background: rgba(0, 0, 0, 0.7); } -.header > .user > ul > li.attn.has-options:hover:before { background: rgba(215, 99, 76, 0.95); } - -/** - * Items for the dropdown menu. - */ -.header > .user > ul > li.has-options > ul > li { - float: left; - width: 50%; - padding: 4px; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; -} - -.header > .user > ul > li.has-options > ul > li > a { - display: block; - padding: 6px 14px; - color: #fff; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - border-radius: 2px; -} - -.header > .user > ul > li.attn.has-options > ul > li > a:hover { - background-color: #d15843; -} - -/** - * The header navigation is floated right and consists of nice squares, love the squares. - */ -.header > .navigation { - float: right; -} - -.header > .navigation > li { - float: left; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - border-radius: 2px; - margin: 0 0 0 3px; -} - -.header > .navigation > li > a { - display: block; - color: #999; - text-shadow: 0 1px 0 #fff; - text-decoration: none; - font-weight: bold; - font-size: 14px; - text-transform: lowercase; - padding: 8px 16px; -} - -.header > .navigation > li:hover { - background: rgba(0, 0, 0, 0.1); -} - -.header > .navigation > li.important { - background: rgba(215, 99, 76, 0.6); -} - -.header > .navigation > li.important:hover { - background: rgba(215, 99, 76, 0.8); -} - -.header > .navigation > li.selected { - background: rgba(0, 0, 0, 0.25); -} - -.header > .navigation > li.selected > a, -.header > .navigation > li.important > a { - color: #fff; - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); -} - -/** - * The main body is a large container that stands out from the background. - */ -.body { - margin: 30px 0 0; - background-color: #fff; - box-shadow: 0 0 3px rgba(0, 0, 0, 0.1); - padding: 10px 10px 20px; - -moz-border-radius: 0 0 2px 2px; - -webkit-border-radius: 0 0 2px 2px; - border-radius: 0 0 2px 2px; -} - -.body .content { - clear: both; -} - -/** - * The breadcrumbs get nice little arrow separators. - */ -.breadcrumbs { - margin: -10px -10px 10px; - background-color: #f2f2f2; - border-bottom: 1px solid #e6e6e6; - text-shadow: 0 1px 0 #fff; - -moz-border-radius: 2px 2px 0 0; - -webkit-border-radius: 2px 2px 0 0; - border-radius: 2px 2px 0 0; - line-height: 40px; -} - -.breadcrumbs > li, -.breadcrumbs > li > a { - color: #666; -} - -.breadcrumbs > li { - display: inline-block; - margin: 0 0 0 -3px; - cursor: default; -} - -.breadcrumbs > li:first-child { margin-left: 0; } - -.breadcrumbs > li, -.breadcrumbs > li > a { - position: relative; - padding: 0 4px 0 26px; - color: #989898; -} - -.breadcrumbs > li > a { - display: block; - text-decoration: none; - margin: 0 -4px 0 -26px; -} - -.breadcrumbs > li:first-child, -.breadcrumbs > li:first-child > a { - position: relative; - padding: 0 4px 0 10px; - color: #989898; -} - -.breadcrumbs > li:first-child > a { - margin: 0 -4px 0 -10px; -} - -.breadcrumbs > li > a:hover, -.breadcrumbs > li > a:focus { - color: #555; -} - -.breadcrumbs > li:hover { - background-color: #f9f9f9; -} - -.breadcrumbs > li:active { - border-color: #f1f1f1; -} - -.breadcrumbs > li:before { - position: absolute; - display: inline-block; - content: ""; - top: -1px; - right: -20px; - border-top: 21px solid rgba(255, 255, 255, 0); - border-bottom: 21px solid rgba(255, 255, 255, 0); - border-left: 19px solid #e2e2e2; - z-index: 1; -} - -.breadcrumbs > li:hover:before, -.breadcrumbs > li:focus:before { - border-left-color: #d7d7d7; -} - -.breadcrumbs > li:after { - position: absolute; - display: inline-block; - content: ""; - top: -1px; - right: -18px; - border-top: 21px solid rgba(255, 255, 255, 0); - border-bottom: 21px solid rgba(255, 255, 255, 0); - border-left: 19px solid #f2f2f2; - z-index: 1; -} - -.breadcrumbs > li:hover:after { - border-left-color: #f9f9f9; -} - -.breadcrumbs > li:hover { text-decoration: none; } - -/** - * Headings appear throughout the body and are a different font and weight - * then other items. - */ -.body h1, -.body h2, -.body h3, -.body h4 { - margin: 0 0 16px; - font-family: "Trebuchet MS", "Ubuntu", serif; - font-size: 24px; - color: #999; - text-transform: uppercase; -} - -.body h2 { font-size: 20px; } -.body h3 { font-size: 18px; } -.body h4 { - font-size: 14px; - display: inline; -} - -.body h1 a, -.body h2 a { - text-decoration: none; -} - -.body p:first-child { margin-top: 0; } -.body p:last-child { margin-bottom: 0; } - -/** - * Body halves allow the page to be split in two equal halves. - */ -.halves > .half-left { - float: left; - width: 49%; -} - -.halves > .half-right { - margin: 0 0 0 51%; -} - -/** - * The footer simply appears under the body and nothing more. - */ -.footer { - font-size: 11px; - padding: 10px 0; - color: #999; -} - -.footer > .powered { - float: left; - margin: 0; -} - -.footer > .stats { - float: right; -} - -.footer > .stats > li { - float: left; - margin: 0 0 0 24px; -} - -.footer > .stats .unit { - display: inline-block; - background-color: #999; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; - color: #fff; - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); - padding: 1px 4px; - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); - font-weight: bold; - margin: 0 4px 0 0; -} \ No newline at end of file diff --git a/app/themes/basic/start.php b/app/themes/basic/start.php deleted file mode 100644 index 45dc4d9..0000000 --- a/app/themes/basic/start.php +++ /dev/null @@ -1,6 +0,0 @@ -add('feather/themes/basic/css/theme.css'); -})->apply('UriRewriteFilter', array($app['path.public'])); \ No newline at end of file diff --git a/app/themes/basic/views/theme.blade.php b/app/themes/basic/views/theme.blade.php deleted file mode 100644 index 5b84081..0000000 --- a/app/themes/basic/views/theme.blade.php +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - @event('view: before template.title') Title - - - {{ Basset::show('basic.css') }} - - - - -
- -
- -
- -
- - - - - -
- -
- - -
- -
-
- - - -
- - - - - - \ No newline at end of file diff --git a/app/views/hello.php b/app/views/hello.php deleted file mode 100644 index ba7c290..0000000 --- a/app/views/hello.php +++ /dev/null @@ -1 +0,0 @@ -

Hello World!

\ No newline at end of file diff --git a/applications/core/controllers/api.php b/applications/core/controllers/api.php new file mode 100644 index 0000000..e11b365 --- /dev/null +++ b/applications/core/controllers/api.php @@ -0,0 +1,37 @@ +header('content-type', 'application/json'); + + $response->content = json_encode($response->content); + break; + } + } + +} \ No newline at end of file diff --git a/applications/core/controllers/api/v1/user.php b/applications/core/controllers/api/v1/user.php new file mode 100644 index 0000000..5dad2d1 --- /dev/null +++ b/applications/core/controllers/api/v1/user.php @@ -0,0 +1,63 @@ +get() as $user) + { + $users[] = $this->data($user); + } + + return $users; + } + + /** + * Return a bunch of user details. + * + * @return array + */ + public function get_bunch() + { + if(!$usernames = json_decode(Input::get('data'))) + { + return array(); + } + + $users = array(); + + foreach(Feather\Core\User::where_in('username', $usernames)->get() as $user) + { + $users[] = $this->data($user); + } + + return $users; + } + + /** + * Returns a user data array. + * + * @param object $user + * @return array + */ + protected function data($user) + { + return array( + 'id' => $user->id, + 'username' => $user->username, + 'email' => $user->email, + 'created_at' => $user->created_at, + 'total_discussions' => $user->total_discussions, + 'total_replies' => $user->total_replies, + 'avatar' => $user->avatar + ); + } + +} \ No newline at end of file diff --git a/applications/core/controllers/base.php b/applications/core/controllers/base.php new file mode 100644 index 0000000..bfd8be4 --- /dev/null +++ b/applications/core/controllers/base.php @@ -0,0 +1,153 @@ +feather = Feather\Components\Support\Facade::application(); + } + + /** + * Determines if a place is valid, if it is return the place. + * + * @param int $id + * @return Feather\Core\Place + */ + protected function place($id) + { + return is_numeric($id) ? Feather\Core\Place::with('permissions')->find($id) : null; + } + + /** + * Determines if a discussion is valid, if it is return the discussion. + * + * @param int $id + * @return Feather\Core\Discussion + */ + protected function discussion($id) + { + return is_numeric($id) ? Feather\Core\Discussion::with(array('participants', 'participants.details'))->find($id) : null; + } + + /** + * Method to be run before each request. + * + * @return void + */ + public function before() + { + if($this->geared) + { + $this->feather['gear']->controller('before', $this); + } + } + + /** + * Method to be run after each request. + * + * @return void + */ + public function after($response) + { + if($this->geared) + { + $this->feather['gear']->controller('after', $this); + } + + $this->feather['gear']->fire('assets: change styles', array(Asset::container('theme'))); + + $this->feather['gear']->fire('assets: change scripts', array(Asset::container('theme'))); + } + + /** + * Overload the execute method on the controller. If the controller is geared we'll + * run the first override gear and return the result accordingly. + * + * @param string $method + * @param array $parameters + * @return string|object + */ + public function execute($method, $parameters = array()) + { + $response = $this->geared ? $this->feather['gear']->controller('override', $this) : null; + + // Only if the response is not null and we actually fired a gear event successfully. + if(!is_null($response) and $response) + { + $response = is_string($response) ? $response : $this->layout; + + return $response; + } + + return parent::execute($method, $parameters); + } + + /** + * Magic method for calling Feather components. + * + * @param string $component + * @return object + */ + public function __get($component) + { + if(isset($this->feather[$component])) + { + return $this->feather[$component]; + } + + throw new BadMethodCallException("Invalid component [{$component}] called on controller."); + } + + /** + * Catch-all method for requests that can't be matched. + * + * @param string $method + * @param array $parameters + * @return Response + */ + public function __call($method, $parameters) + { + $response = $this->geared ? $this->feather['gear']->controller('create', $this) : null; + + if(is_null($response) or !$response) + { + return Response::error('404'); + } + } + +} \ No newline at end of file diff --git a/applications/core/controllers/discussion.php b/applications/core/controllers/discussion.php new file mode 100644 index 0000000..a3b903b --- /dev/null +++ b/applications/core/controllers/discussion.php @@ -0,0 +1,316 @@ +filter('before', 'csrf')->on(array('post', 'put'))->except(array('preview')); + } + + /** + * Read a discussion. + * + * @return object + */ + public function get_index($id, $slug) + { + // Make sure that the user has the correct permissions to view this discussion. + if($discussion = $this->discussion($id) and $this->auth->can('view: discussion', $discussion)) + { + // If the discussion is a draft, they cannot view it. Take them straight to the edit page. + if($discussion->draft) + { + return $this->redirect->to_route('discussion.edit', array($discussion->id, $discussion->slug)); + } + + $discussion->build($this->config->get('feather: db.replies.per_page')); + + $this->breadcrumbs->drop($discussion->place)->drop($discussion->title); + + $this->layout->nest('content', 'feather core::discussion.view', compact('discussion')) + ->with('title', $discussion->title); + } + + // User cannot view this discussion so the discussion is not found. + else + { + $this->breadcrumbs->drop(__('feather core::discussion.discussion_not_found.title')); + + $error = array( + 'title' => __('feather core::discussion.discussion_not_found.title'), + 'error' => __('feather core::discussion.discussion_not_found.message'), + 'type' => 'error' + ); + + $this->layout->nest('content', 'feather core::error.page', compact('error')) + ->with('title', __('feather core::discussion.discussion_not_found.title')); + } + } + + /** + * Allows a new discussion to be started on a given place. If no place is provided the + * user can select a place. + * + * @param int $place + * @return void + */ + public function get_start($place = null) + { + $places = Feather\Core\Place::options(array( + 'selected' => $place, + 'permissions' => true, + 'action' => 'start: discussions', + 'cascade' => false + )); + + $place = $this->place($place); + + // Make sure the user has the correct permissions to start a discussion and that there are + // places to start a discussion on. + if($this->auth->can('start: discussions', $place) and $places) + { + $this->breadcrumbs->drop(__('feather core::titles.start_discussion')); + + $preview = (Input::had('preview') and Input::had('body')) ? + View::make('feather core::discussion.preview')->with('body', Feather\Gear\Markdown\Parse(Input::old('body'))) : + null; + + $this->layout->nest('content', 'feather core::discussion.start', compact('places', 'preview')) + ->with('title', __('feather core::titles.start_discussion')); + } + else + { + // The user cannot start a discussion on this place in particular. + if($this->auth->cannot('start: discussions', $place)) + { + $this->breadcrumbs->drop(__('feather core::discussion.cannot_start_discussion.title')); + + $error = array( + 'title' => __('feather core::discussion.cannot_start_discussion.title'), + 'error' => __('feather core::discussion.cannot_start_discussion.message'), + 'type' => 'error' + ); + + $this->layout->with('title', __('feather core::discussion.cannot_start_discussion.title')); + } + + // The user cannot start a discussion on any available places. + else + { + $this->breadcrumbs->drop(__('feather core::discussion.no_places_for_discussion.title')); + + $error = array( + 'title' => __('feather core::discussion.no_places_for_discussion.title'), + 'error' => __('feather core::discussion.no_places_for_discussion.message'), + 'type' => 'error' + ); + + $this->layout->with('title', __('feather core::discussion.no_places_for_discussion.title')); + } + + $this->layout->nest('content', 'feather core::error.page', compact('error')); + } + } + + /** + * Attempts to start a new discussion on the place provided. + * + * @param int $place + * @return void + */ + public function post_start($place = null) + { + if(Input::has('preview')) + { + return $this->redirect->to_self()->with_input(); + } + + if($this->auth->cannot('start: discussions', $this->place(Input::get('place')))) + { + return $this->redirect->to_self() + ->with_input() + ->with_alert('error', 'feather core::discussion.cannot_start_discussion.message'); + } + + Input::merge(array('user' => $this->auth->user->id)); + + try + { + $this->validator->get('discussion.start')->against(Input::get())->passes(); + } + catch (FeatherValidationException $errors) + { + return $this->redirect->to_self() + ->with_input() + ->with_errors($errors->get()); + } + + try + { + $discussion = Feather\Core\Discussion::start(Input::get()); + } + catch (FeatherModelException $errors) + { + return $this->redirect->to_self() + ->with_input() + ->with_alert('error', 'feather core::discussion.cannot_start_discussion.message'); + } + + if(Input::has('draft')) + { + return $this->redirect->to_route('discussion.edit', array($discussion->id, $discussion->slug)) + ->with_alert('success', 'feather core::discussion.draft_saved'); + } + else + { + return $this->redirect->to_route('discussion', array($discussion->id, $discussion->slug)) + ->with_alert('success', 'feather core::discussion.started'); + } + } + + /** + * Allows users to edit a discussion if it belongs to them, or if they are a moderator or + * administrator. + * + * @param int $id + * @param string $slug + * @return void + */ + public function get_edit($id, $slug) + { + // Make sure the user has the correct permissions to edit this discussion. + if($discussion = $this->discussion($id) and $this->auth->can('edit: discussion', $discussion)) + { + $this->breadcrumbs->drop($discussion->place)->drop(__('feather core::titles.edit_discussion', array('discussion' => $discussion->title))); + + $participants = $discussion->participants_to_string(); + + $places = Feather\Core\Place::options(array( + 'selected' => $discussion->place_id, + 'permissions' => true, + 'action' => 'start: discussions', + 'cascade' => false + )); + + $preview = (Input::had('preview') and Input::had('body')) ? + View::make('feather core::discussion.preview')->with('body', Feather\Gear\Markdown\Parse(Input::old('body'))) : + null; + + $this->layout->nest('content', 'feather core::discussion.edit', compact('discussion', 'places', 'participants', 'preview')) + ->with('title', __('feather core::titles.edit_discussion', array('discussion' => $discussion->title))); + } + else + { + // The discussion the user is attempting to edit could not be found. + if(!$discussion) + { + $this->breadcrumbs->drop(__('feather core::discussion.discussion_not_found.title')); + + $error = array( + 'title' => __('feather core::discussion.discussion_not_found.title'), + 'error' => __('feather core::discussion.discussion_not_found.message'), + 'type' => 'error' + ); + + $this->layout->with('title', __('feather core::discussion.discussion_not_found.title')); + } + + // The user does not have the permission to edit this discussion. + else + { + $this->breadcrumbs->drop(__('feather core::discussion.cannot_edit_discussion.title')); + + $error = array( + 'title' => __('feather core::discussion.cannot_edit_discussion.title'), + 'error' => __('feather core::discussion.cannot_edit_discussion.message'), + 'type' => 'error' + ); + + $this->layout->with('title', __('feather core::discussion.cannot_edit_discussion.title')); + } + + $this->layout->nest('content', 'feather core::error.page', compact('error')); + } + } + + /** + * Attempts to edit a discussion. + * + * @param int $id + * @param string $slug + * @return void + */ + public function put_edit($id, $slug) + { + if(Input::has('preview')) + { + return $this->redirect->to_self()->with_input(); + } + + if(!$discussion = $this->discussion($id)) + { + return $this->redirect->to_route('feather') + ->with_alert('error', 'feather core::discussion.invalid'); + } + + if($this->auth->cannot('start: discussions', $this->place(Input::get('place')))) + { + return $this->redirect->to_self() + ->with_input() + ->with_alert('error', 'feather core::discussion.cannot_start_discussion.message'); + } + + Input::merge(array('user' => $this->auth->user->id)); + + try + { + $this->validator->get('discussion.edit')->against(Input::get())->passes(); + } + catch (FeatherValidationException $errors) + { + return $this->redirect->to_self() + ->with_input() + ->with_errors($errors->get()); + } + + try + { + $discussion = Feather\Core\Discussion::edit($discussion, Input::get()); + } + catch (FeatherModelException $errors) + { + return $this->redirect->to_self() + ->with_input() + ->with_alert('error', 'feather core::discussion.edit_failure.message'); + } + + if(Input::has('draft')) + { + return $this->redirect->to_route('discussion.edit', array($discussion->id, $discussion->slug)) + ->with_alert('success', 'feather core::discussion.draft_saved'); + } + else + { + $redirect = $this->redirect->to_route('discussion', array($discussion->id, $discussion->slug)); + + if(Input::has('save')) + { + $redirect->with_alert('success', 'feather core::discussion.saved'); + } + else + { + $redirect->with_alert('success', 'feather core::discussion.started'); + } + + return $redirect; + } + } + +} \ No newline at end of file diff --git a/applications/core/controllers/index.php b/applications/core/controllers/index.php new file mode 100644 index 0000000..3508889 --- /dev/null +++ b/applications/core/controllers/index.php @@ -0,0 +1,150 @@ +config->get('feather: db.overview.discussions_per_place')); + + $this->layout->nest('content', 'feather core::index.home', compact('places')); + } + + /** + * Allows a user to enter their login details into a form and then become + * authenticated on the forums. + * + * @return void + */ + public function get_login() + { + if($this->auth->online()) + { + return $this->redirect->to_route('feather'); + } + elseif($this->config->get('feather: db.auth.driver', 'internal') != 'internal') + { + return $this->redirect->before_login(); + } + + $this->breadcrumbs->drop(__('feather core::titles.login')); + + $this->layout->with('title', __('feather core::titles.login')) + ->nest('content', 'feather core::user.login'); + } + + /** + * Handles the posting of the login form and after validation, attempts to + * authenticate the user. + * + * @return void + */ + public function post_login() + { + try + { + $this->validator->get('auth.login')->against(Input::get())->passes(); + } + catch (FeatherValidationException $errors) + { + return $this->redirect->with_query('route: login')->with_input()->with_errors($errors->get()); + } + + if($this->auth->attempt(Input::get())) + { + return $this->redirect->to_previous('route: feather'); + } + + return $this->redirect->to_self()->with_input()->alert('error', 'feather core::login.failure'); + } + + /** + * Logs a user out of the forums and redirects them back to where they were, or depending + * on the authentication driver, a logout landing page. + * + * @return void + */ + public function get_logout() + { + $this->auth->logout(); + + // Return to the generated logout URL, this can change depending on the authentication + // driver that is being used. + return $this->redirect->after_logout(); + } + + /** + * If registrations are enabled a user can enter the required details into the registration + * form and register an account on the forums. + * + * @return void + */ + public function get_register() + { + if($this->config->get('feather: db.auth.driver', 'internal') != 'internal') + { + return $this->redirect->before_register(); + } + + $this->breadcrumbs->drop(__('feather core::titles.register')); + + $this->layout->with('title', __('feather core::titles.register')) + ->nest('content', 'feather core::user.register'); + } + + /** + * Handles the posting of the registration form. Validates and then attempts to + * register a user with the details they supplied. If e-mail activation is enabled + * a user will also be sent an e-mail so they can activate their account. + * + * @return void + */ + public function post_register() + { + try + { + $this->validator->get('auth.register')->against(Input::get())->passes(); + } + catch (FeatherValidationException $errors) + { + return $this->redirect->to_self()->with_input()->with_errors($errors->get()); + } + + try + { + $user = Feather\Core\User::register(Input::get()); + } + catch (FeatherModelException $errors) + { + return $this->redirect->to_self()->with_input()->alert('error', 'feather core::register.failure'); + } + + // Users must confirm their e-mail address once they have registered. We + // will now send them an e-mail with their activation code. + if($this->config->get('feather: db.registration.confirm_email')) + { + + } + + $this->auth->login($user); + + return $this->redirect->to_previous('route: feather'); + } + + /** + * Simply shows the forums rules. + * + * @return void + */ + public function get_rules() + { + $this->layout->with('title', __('feather core::titles.rules')) + ->nest('content', 'feather core::misc.rules'); + } + +} \ No newline at end of file diff --git a/applications/core/controllers/place.php b/applications/core/controllers/place.php new file mode 100644 index 0000000..d83e453 --- /dev/null +++ b/applications/core/controllers/place.php @@ -0,0 +1,18 @@ +config->get('feather: db.discussions.per_page'))) + { + + } + + $this->breadcrumbs->drop($place); + + $this->layout->nest('content', 'feather core::place.view', compact('place')) + ->with('title', $place->name); + } + +} \ No newline at end of file diff --git a/applications/core/language/en/common.php b/applications/core/language/en/common.php new file mode 100644 index 0000000..bb62e2c --- /dev/null +++ b/applications/core/language/en/common.php @@ -0,0 +1,23 @@ + 'Username', + 'password' => 'Password', + 'email' => 'E-mail', + 'login' => 'Sign In', + 'register' => 'Create Account', + 'connect_with_existing_service' => 'Connect with an existing service', + 'today' => 'Today', + 'yesterday' => 'Yesterday', + 'rules' => 'Rules' +); \ No newline at end of file diff --git a/applications/core/language/en/discussion.php b/applications/core/language/en/discussion.php new file mode 100644 index 0000000..205aa8e --- /dev/null +++ b/applications/core/language/en/discussion.php @@ -0,0 +1,146 @@ + array( + 'title' => 'Discussion Not Found', + 'message' => 'Oh shoot! We were unable to locate the discussion you have request. This may be due to a number of reasons, including the discussion being removed or incorrect permissions.' + ), + + /* + |-------------------------------------------------------------------------- + | Cannot Edit Discussion + |-------------------------------------------------------------------------- + | + | English language message for when a discussion cannot be found. + | + */ + + 'cannot_edit_discussion' => array( + 'title' => 'Cannot Edit Discussion', + 'message' => 'Hold up there! You do not have the correct permissions to be editing this discussion.' + ), + + /* + |-------------------------------------------------------------------------- + | No Places For Discussions + |-------------------------------------------------------------------------- + | + | English language message for when their are no places available to start + | a new discussion. + | + */ + + 'no_places_for_discussion' => array( + 'title' => 'Cannot Start Discussion', + 'message' => 'Uh-oh! You are current unable to start a new discussion because there appear to be no places you can start your discussion. Please try starting a new discussion when there are possible places.' + ), + + /* + |-------------------------------------------------------------------------- + | Cannot Start Discussion + |-------------------------------------------------------------------------- + | + | English language message for when user has incorrect permissions to start + | a discussion on the selected place. + | + */ + + 'cannot_start_discussion' => array( + 'title' => 'Cannot Start Discussion', + 'message' => 'Hold up there! You do not have the correct permissions to start a discussion on this place.' + ), + + /* + |-------------------------------------------------------------------------- + | Edit Failure + |-------------------------------------------------------------------------- + | + | English language message for when user failed to edit a discussion. + | + */ + + 'edit_failure' => array( + 'title' => 'Discussion Edit Failure', + 'message' => 'Oh shoot! The discussion could not be edited, there may have been a database related problem. Please try again.' + ), + + /* + |-------------------------------------------------------------------------- + | Discussion Validation Messages + |-------------------------------------------------------------------------- + | + | English language messages for individual field errors. + | + */ + + 'messages' => array( + 'body' => array('is_required' => 'You need to write something for your discussion.'), + 'title' => array('is_required' => 'You need a title for your discussion.'), + 'place' => array('is_required' => 'You need to select a place for your discussion.') + ), + + /* + |-------------------------------------------------------------------------- + | Discussion Form Labels + |-------------------------------------------------------------------------- + | + | English language messages for discussion page form labels. + | + */ + + 'labels' => array( + 'place' => array( + 'title' => 'Place', + 'helper' => 'Where do you want to start this discussion?' + ), + 'title' => array( + 'title' => 'Discussion title' + ), + 'participants' => array( + 'title' => 'Discussion participants', + 'helper' => 'List a person or group of people who can participate in this discussion, by default anyone can participate.' + ), + 'save_draft' => array( + 'title' => 'Save Draft', + 'helper' => 'Don\'t worry, people can\'t see it.' + ) + ), + + /* + |-------------------------------------------------------------------------- + | Discussion Strings + |-------------------------------------------------------------------------- + | + | English language strings for discussion related tasks. + | + */ + + 'anyone_can_participate' => 'Anyone can participate in this discussion.', + 'start_discussion' => 'Start Discussion', + 'save_discussion' => 'Save Discussion', + 'save_draft' => 'Save Draft', + 'preview_discussion' => 'Preview Discussion', + 'draft_saved' => 'Awesome! Your changes to the discussion draft have been saved.', + 'started' => 'Great stuff! Your discussions has now been started. Take a look!', + 'saved' => 'Thanks for that! Your changes to the discussion have been saved.' + +); \ No newline at end of file diff --git a/applications/core/language/en/login.php b/applications/core/language/en/login.php new file mode 100644 index 0000000..360aa4b --- /dev/null +++ b/applications/core/language/en/login.php @@ -0,0 +1,58 @@ + 'Your username and/or password is incorrect.', + + /* + |-------------------------------------------------------------------------- + | Login Validation Messages + |-------------------------------------------------------------------------- + | + | English language messages for individual field errors. + | + */ + + 'messages' => array( + 'username' => array('is_required' => 'You did not enter a username.'), + 'password' => array('is_required' => 'You did not enter a password.') + ), + + /* + |-------------------------------------------------------------------------- + | Login Form Labels + |-------------------------------------------------------------------------- + | + | English language messages for login page form labels. + | + */ + + 'labels' => array( + 'remember' => array( + 'title' => 'Remember me', + 'helper' => 'If yes, when you next return you\'ll be automatically signed in.' + ), + 'register' => array( + 'title' => 'No account? Create a new account today!' + ) + ) + +); \ No newline at end of file diff --git a/applications/core/language/en/place.php b/applications/core/language/en/place.php new file mode 100644 index 0000000..344cb05 --- /dev/null +++ b/applications/core/language/en/place.php @@ -0,0 +1,16 @@ + 'There are no discussions to show for this place.' + +); \ No newline at end of file diff --git a/applications/core/language/en/register.php b/applications/core/language/en/register.php new file mode 100644 index 0000000..d36e3cc --- /dev/null +++ b/applications/core/language/en/register.php @@ -0,0 +1,81 @@ + 'Your registration was unsuccessful. Please try again later or get in touch with an admin.', + + /* + |-------------------------------------------------------------------------- + | Register Validation Messages + |-------------------------------------------------------------------------- + | + | English language messages for individual field errors. + | + */ + + 'messages' => array( + 'username' => array( + 'is_required' => 'You did not enter a username.', + 'too_short' => 'Your username must be at least :length characters.', + 'too_long' => 'Your username must be less than :length characters.', + 'is_invalid' => 'Your username contained invalid characters.', + 'already_exists' => 'An account already exists with that username.' + ), + 'password' => array( + 'is_required' => 'You did not enter a password.', + 'too_short' => 'Your password must be at least :length characters.', + 'too_long' => 'Your password must be less than :length characters.', + 'did_not_match' => 'The passwords you entered did not match.', + ), + 'password_confirmation' => array( + 'is_required' => 'You did not confirm your password.', + ), + 'email' => array( + 'is_required' => 'You did not enter an e-mail address.', + 'invalid' => 'The e-mail you entered is invalid.', + 'already_exists' => 'An account already exists with that e-mail.', + 'does_not_exist' => 'The e-mail you entered does not exist.' + ), + 'rules' => array( + 'not_accepted' => 'You must read and accept the community rules.' + ) + ), + + /* + |-------------------------------------------------------------------------- + | Register Form Labels + |-------------------------------------------------------------------------- + | + | English language messages for register page form labels. + | + */ + + 'labels' => array( + 'password_confirmation' => array( + 'title' => 'Confirm Password' + ), + 'rules' => array( + 'helper' => 'I\'ve read and agree to the :link.' + ) + ) + + +); \ No newline at end of file diff --git a/applications/core/language/en/titles.php b/applications/core/language/en/titles.php new file mode 100644 index 0000000..cb6057f --- /dev/null +++ b/applications/core/language/en/titles.php @@ -0,0 +1,20 @@ + 'Home', + 'login' => 'Sign In', + 'register' => 'Create Account', + 'rules' => 'Rules', + 'connect_to_community' => 'Connect to Community', + 'start_discussion' => 'Start Discussion', + 'edit_discussion' => 'Editing Discussion: :discussion' +); \ No newline at end of file diff --git a/applications/core/models/base.php b/applications/core/models/base.php new file mode 100644 index 0000000..e553e13 --- /dev/null +++ b/applications/core/models/base.php @@ -0,0 +1,99 @@ + array('place', 'place_id'), + 'user' => array('user', 'user_id'), + 'author' => array('user', 'user_id') + ); + + /** + * Cache time in minutes. + * + * @var int + */ + const cache_time = 720; + + /** + * Log a query that can not or will not be logged by the normal query logger. + * + * @param string $sql + * @param int $time + * @param array $bindings + * @return void + */ + protected static function log($sql, $time, $bindings = array()) + { + if(Config::get('laravel: database.profile')) + { + Event::fire('laravel.query', array($sql, $bindings, number_format((microtime(true) - $time) * 1000, 2))); + } + } + + /** + * Find a model by its primary key. + * + * @param string $id + * @param array $columns + * @return Model + */ + public function _find($id, $columns = array('*')) + { + $key = explode('\\', get_called_class()); + $key = Str::lower(array_pop($key)); + + if(isset(static::$cachable[$key])) + { + list($group, $foreign) = static::$cachable[$key]; + + if(Cache::has("{$group}_{$id}")) + { + return Cache::get("{$group}_{$id}"); + } + } + + return $this->query()->where(static::$key, '=', $id)->first($columns); + } + + /** + * Handle the dynamic retrieval of attributes and associations. + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + if(isset(static::$cachable[$key]) and !isset($this->attributes[$key]) and !isset($this->relationships[$key])) + { + list($group, $foreign) = static::$cachable[$key]; + + if(Cache::has("{$group}_{$this->$foreign}")) + { + return Cache::get("{$group}_{$this->$foreign}"); + } + } + + // Of course if it's not in there we'll just continue on as per normal. + return parent::__get($key); + } + +} \ No newline at end of file diff --git a/applications/core/models/discussion.php b/applications/core/models/discussion.php new file mode 100644 index 0000000..fc12f3f --- /dev/null +++ b/applications/core/models/discussion.php @@ -0,0 +1,494 @@ +belongs_to('Feather\\Core\\User', 'user_id'); + } + + /** + * A discussion might have a recent poster, otherwise it has nothing. + * + * @return object + */ + public function recent() + { + return $this->belongs_to('Feather\\Core\\User', 'last_reply_user_id'); + } + + /** + * A discussion has a place. + * + * @return object + */ + public function place() + { + return $this->belongs_to('Feather\\Core\\Place', 'place_id'); + } + + /** + * A discussion can have many participants. + * + * @return object + */ + public function participants() + { + return $this->has_many('Feather\\Core\\Discussion\\Participant', 'discussion_id'); + } + + /** + * A discussion can have many replies. + * + * @return object + */ + public function replies() + { + return $this->has_many('Feather\\Core\\Reply', 'discussion_id'); + } + + /** + * Getter for a discussions slug. + * + * @return string + */ + public function get_slug() + { + return Str::slug($this->get_attribute('title')); + } + + /** + * Getter for the replies. Turns it into a readable format. + * + * @return string + */ + public function get_short_replies() + { + return $this->shortner($this->get_attribute('replies')); + } + + /** + * Getter for the views. Turns it into a readable format. + * + * @return string + */ + public function get_short_views() + { + return $this->shortner($this->get_attribute('views')); + } + + /** + * Shorten the replies and views numbers + * + * @param int $number + * @return string + */ + protected function shortner($number) + { + // Oh wow, under a 1,000... Not bad. + if($number < 1000) + { + $number = number_format($number); + } + // Hold up, getting up in the thousands! + elseif($number < 1000000) + { + $number = number_format($number / 1000, 1) . 'K'; + } + // Ah-hoy! A million! + elseif($number < 1000000000) + { + $number = number_format($number / 1000000, 1) . 'M'; + } + // CRIKEY! A biollion!? + else + { + $number = number_format($number / 1000000000, 1) . 'B'; + } + + return str_replace('.0', '', $number); + } + + /** + * Start a new discussion. + * + * @param array $input + * @return object + */ + public static function start($input) + { + return static::edit(new static, $input); + } + + /** + * Edit an existing discussion. + * + * @param object $discussion + * @param array $input + * @return object + */ + public static function edit($discussion, $input) + { + $increment = false; + + // If the discussion exists then we are only going to increment the total + // discussions for the place if the discussion is being saved from a draft + // to public viewing. + if($discussion->exists) + { + $increment = $discussion->draft ? (isset($input['draft']) ? false : true) : false; + + // We'll also update the created_at and updated_at timestamps here if it's being + // started from a draft. Otherwise an old draft wouldn't be shown at the top of the + // discussions list. + if(isset($input['start'])) + { + $discussion->fill(array( + 'created_at' => new DateTime, + 'updated_at' => new DateTime + )); + } + } + + // If the discussion doesn't exist we only increment the total discussions + // if the discussion is being started, not drafted. + else + { + $increment = isset($input['start']); + } + + $place = Place::find($input['place']); + + $discussion->fill(array( + 'place_id' => $input['place'], + 'user_id' => $input['user'], + 'title' => isset($input['draft']) ? (empty($input['title']) ? 'Untitled Discussion' : $input['title']) : (isset($input['title']) ? $input['title'] : $discussion->title), + 'body' => $input['body'], + 'private' => empty($input['participants']) ? 0 : 1, + 'draft' => isset($input['draft']) ? 1 : 0 + )); + + if(!$discussion->save()) + { + throw new FeatherModelException; + } + + // If starting a discussion or saving a drafted discussion for public viewing we + // increment the places total discussions by one. + if($increment) + { + $place->total_discussions += 1; + + $place->save(); + + // We can now increment the users discussion count by one if the place allows + // for a user post count increase. + if($place->user_post_increment) + { + $user = User::find($input['user']); + + $user->total_discussions += 1; + + $user->save(); + } + } + + $participants = array(); + + foreach(explode(',', $input['participants']) as $participant) + { + $participants[] = trim($participant); + } + + $participants = array_filter($participants); + + // Grab all the participants info from the database, make sure they're all valid then + // use their IDs. + if($participants and $users = User::where_in('username', $participants)->get()) + { + $participants = array(); + + foreach($users as $key => $user) + { + $participants[$user->id] = array( + 'user_id' => $user->id, + 'discussion_id' => $discussion->id + ); + } + } + + // If this discussion is private we need to add the participants to the participants + // table. Sync the table, remove any that were removed and add any new ones. + if($discussion->participants) + { + // First thing is to delete any participants where the IDs are not in the + // submitted participants for this discussion. + if($participants) + { + Discussion\Participant::where_not_in('user_id', array_keys($participants))->where_discussion_id($discussion->id)->delete(); + } + else + { + Discussion\Participant::where_discussion_id($discussion->id)->delete(); + } + + // Spin through the discussions existing participants and remove them from the + // array if they are already participating. + foreach($discussion->participants as $participant) + { + if(isset($participants[$participant->id])) + { + unset($participants[$participant->id]); + } + } + } + + if($participants) + { + Discussion\Participant::insert($participants); + } + + // Clear the cache for this discussion, as well as the place it belongs, all places, + // and the current user. + Cache::forget("discussion_{$discussion->id}"); + + Cache::forget("place_{$discussion->place_id}"); + + Cache::forget('places'); + + Cache::forget("user_{$discussion->user_id}"); + + return $discussion; + } + + /** + * Returns an enriched array of discussions with relationships loaded. + * + * @param array $discussions + * @return array + */ + public static function enrichment($discussions) + { + if(!is_array($discussions)) + { + $discussions = array($discussions); + } + + $ids = array( + 'participants' => array(), + 'author' => array(), + 'recent' => array() + ); + + foreach($discussions as $discussion) + { + $ids['author'][] = $discussion->user_id; + + $ids['participants'][] = $discussion->id; + + if($discussion->last_reply_user_id) + { + $ids['recent'][] = $discussion->last_reply_user_id; + } + } + + $ids['author'] = array_unique($ids['author']); + + $ids['recent'] = array_unique($ids['recent']); + + // Here we will run three queries, one to fetch all the authors, another to fetch all the participants + // and the last will fetch the last poster details, if any. + $authors_raw = $ids['author'] ? User::where_in('id', $ids['author'])->get() : array(); + + $participants_raw = $ids['participants'] ? Discussion\Participant::with('details')->where_in('discussion_id', $ids['participants'])->get() : array(); + + $recently_raw = $ids['recent'] ? User::where_in('id', $ids['recent'])->get() : array(); + + // We now need to sort the arrays better, because at the moment accessing objects within the arrays + // is very hard. The keys for each array need to be the corrosponding foreign keys for the relationships. + $authors = array(); + foreach($authors_raw as $author) $authors[$author->id] = $author; + + $participants = array(); + foreach($participants_raw as $participant) $participants[$participant->discussion_id][$participant->id] = $participant; + + $recently = array(); + + if($recently_raw) + { + foreach($recently_raw as $recent) $recently[$recent->id] = $recent; + } + + // Now that the arrays are sorted all lovely like we can spin through our discsussions again and + // assign the relationships to each discussion. + foreach($discussions as $discussion) + { + $discussion->relationships['author'] = $authors[$discussion->user_id]; + + if(isset($participants[$discussion->id])) + { + $discussion->relationships['participants'] = $participants[$discussion->id]; + } + else + { + $discussion->relationships['participants'] = null; + } + + if($discussion->last_reply_user_id) + { + $discussion->relationships['recent'] = $recently[$discussion->last_reply_user_id]; + } + else + { + $discussion->relationships['recent'] = null; + } + } + + return $discussions; + } + + /** + * Builds a discussion bringing in it's replies and all related data. This essentially + * rewrites most of the object since the discussion itself is included as a reply. + * + * @param int $replies_per_page + * @return array + */ + public function build($replies_per_page = 15) + { + // Determine what discussions to select. Remembering to include the discussion itself as a reply. + // Otherwise our pages would be all messed up. + $page = Paginator::page($total_results = $this->replies()->count(), $replies_per_page); + + $skip = ($page - 1) * $replies_per_page; + + // If we are on the first page we need to include the actual discussion, so when selecting the replies + // only take the replies per page minus one. + $take = ($page == 1) ? ($replies_per_page - 1) : $replies_per_page; + + + + // Spin through the replies and build an array of IDs, we need to get some extra details for each of the replies, + // such as author, editor, and deleter. + $replies = $users = array(); + + $ids = array($this->user_id); + + foreach((array) $this->replies()->skip($skip)->take($take)->get() as $reply) + { + $replies[$reply->id] = $reply; + + $ids[] = $reply->user_id; + + if($reply->edited_id) + { + $ids[] = $reply->edited_id; + } + + if($reply->deleted_id) + { + $ids[] = $reply->deleted_id; + } + } + + $ids = array_unique($ids); + + foreach(($ids ? User::where_in('id', $ids)->get() : array()) as $user) + { + $users[$user->id] = $user; + } + + // Add the related user accounts to each reply. + foreach($replies as $reply) + { + $reply->relationships['author'] = $users[$reply->user_id]; + + $reply->relationships['deleter'] = $reply->deleted ? $users[$reply->deleted_id] : array(); + + $reply->relationships['editor'] = $reply->edited ? $users[$reply->edited_id] : array(); + } + + if($page == 1) + { + // We'll turn the discussion into a reply and merge it into the replies, then we'll perform our pagination + // of the discussion. + $discussion = new Reply(array( + 'user_id' => $this->user_id, + 'discussion_id' => $this->id, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'body' => $this->body, + 'deleted' => 0, + 'edited' => 0 + ), true); + + $discussion->relationships['author'] = $users[$this->user_id]; + + $discussion->relationships['deleter'] = array(); + + $discussion->relationships['editor'] = array(); + + $replies = array($discussion) + $replies; + } + + $pagination = Paginator::make($replies, $total_results, $replies_per_page); + + $this->relationships['replies'] = $pagination->results; + + $this->pagination = $pagination->links(); + + return $this; + } + + /** + * Converts participant IDs into a comma separated string. + * + * @return string + */ + public function participants_to_string() + { + $ids = $participants = array(); + + foreach($this->participants as $participant) + { + $ids[] = $participant->user_id; + } + + if($ids and $users = User::where_in('id', $ids)->get()) + { + foreach($users as $user) + { + $participants[] = $user->username; + } + } + + return implode(', ', $participants); + } + +} \ No newline at end of file diff --git a/applications/core/models/discussion/participant.php b/applications/core/models/discussion/participant.php new file mode 100644 index 0000000..21e04f6 --- /dev/null +++ b/applications/core/models/discussion/participant.php @@ -0,0 +1,31 @@ +belongs_to('Feather\\Core\\User', 'user_id'); + } + +} \ No newline at end of file diff --git a/applications/core/models/gear.php b/applications/core/models/gear.php new file mode 100644 index 0000000..2be0a72 --- /dev/null +++ b/applications/core/models/gear.php @@ -0,0 +1,29 @@ +get(); + } + +} \ No newline at end of file diff --git a/applications/core/models/migration.php b/applications/core/models/migration.php new file mode 100644 index 0000000..40f4384 --- /dev/null +++ b/applications/core/models/migration.php @@ -0,0 +1,22 @@ +first(); + } + +} \ No newline at end of file diff --git a/applications/core/models/ness.php b/applications/core/models/ness.php new file mode 100644 index 0000000..10b92ee --- /dev/null +++ b/applications/core/models/ness.php @@ -0,0 +1,477 @@ + + * + * @author Jason Lewis + * @version 1.0.2 + */ +class Ness extends Base { + + /** + * Stores the Ness related data to avoid cluttering the Eloquent-space. + * + * @var bool + */ + protected $ness = array( + 'deleting' => false, + 'crumbs' => null + ); + + /** + * Place a node after another node in the tree. + * + * + * $node = Ness::find(4); + * + * // Place the node after the node with an ID of 2. + * $node->after(2); + * + * // Place the node after another Ness object. + * $node->after(Ness::find(2)); + * + * + * @param object|int $node + * @return object + */ + public function after($node) + { + return $this->sibling($node, 'after'); + } + + /** + * Place a node before another node in the tree. + * + * + * $node = Ness::find(4); + * + * // Place the node before the node with an ID of 2. + * $node->before(2); + * + * // Place the node before another Ness object. + * $node->before(Ness::find(2)); + * + * + * @param object|int $node + * @return object + */ + public function before($node) + { + return $this->sibling($node, 'before'); + } + + /** + * Makes a node a sibling of another node, the position can either be before or after. + * + * @param object|int $node + * @param string $position + * @return object + */ + protected function sibling($node, $position) + { + $node = $this->check($node); + + // If our node currently exists we need to fake its death, shifting it outside of the + // tree momenterily. + if($this->exists) + { + $this->fake(); + + // Refresh the node we are placing it after so we can get the latest right value. + $node->refresh(); + + // And now revive the bastard to the new left position. + $this->revive(($position == 'after') ? $node->rgt : $node->lft - 1); + } + + // If the node doesn't exist yet in the tree we need to set the left and right values. + // If our position is BEFORE our left value will be the siblings current left value, if + // it is AFTER then it will be the right value plus 1. The right value will always be + // the left value plus 1. + else + { + $this->lft = ($position == 'after') ? $node->rgt + 1 : $node->lft; + + $this->rgt = $this->lft + 1; + + // We need to make an adjustment to the tree so we can fit out brand new node in. + // Once done we'll save our new node. + $this->adjustment($this->lft) + ->save(); + } + + return $this; + } + + /** + * Nest the current node on the supplied node. + * + * + * $node = Ness::find(3); + * + * // Nest the node on node with an ID of 5. + * $node->nest(5); + * + * // Nest the node on another Ness object. + * $node->nest(Ness::find(5)); + * + * + * @param object|int $node + * @return object + */ + public function nest($node) + { + $node = $this->check($node); + + if(!$node->exists) + { + throw new Exception('The node you are nesting on has not been saved yet.'); + } + + // If our node currently exists we need to fake its death, shifting it outside of the + // tree momenterily. + if($this->exists) + { + $this->fake(); + + // Refresh the node we are placing it after so we can get the latest right value. + $node->refresh(); + + // And now revive the bastard to the parent nodes right value. + $this->revive($node->rgt - 1); + + // Refresh our node. + $this->refresh(); + } + + // If the node doesn't exist yet we need set the left and right values. The new node will + // always be placed at the end of any other children. + else + { + $this->lft = $node->rgt; + + $this->rgt = $this->lft + 1; + + // We need to make an adjustment to the tree so we can fit out brand new node in. + // Once done we'll save our new node. + $this->adjustment($this->lft - 1) + ->save(); + } + } + + /** + * Parent node abandons children nodes, they become orphans of the next parent. + * + * + * $parent = Ness::find(4); + * + * // Abandon the children making them orphans to the parents parent. + * $parent->abandon(); + * + * // Abandon the children then move the node to another position. + * $parent->abandon()->before(2); + * + * + * @return object + */ + public function abandon() + { + if(!$this->parent()) + { + throw new Exception('Could not abandon children because the node is not a parent.'); + } + + // Shift all the children of the node forward one position. + static::where('lft', 'BETWEEN', DB::raw(($this->lft + 1) . ' AND ' . ($this->rgt - 1)))->update(array( + 'lft' => DB::raw('lft + 1'), + 'rgt' => DB::raw('rgt + 1') + )); + + $this->rgt = $this->lft + 1; + + $this->save(); + + return $this; + } + + /** + * Deletes a node and any children of that node unless the node has abandoned its children. + * + * + * $node = Ness::find(3); + * + * // Delete the node including it's children if it has any. + * $node->delete(); + * + * // To save the children abandon them before deleting. + * $node->abandon()->delete(); + * + * + * @return object + */ + public function delete() + { + if($this->exists) + { + // If we are deleting then we'll allow Eloquent to take care of the rest. + if($this->ness['deleting']) + { + parent::delete(); + + $this->ness['deleting'] = false; + } + + // If not deleting yet we'll need to check what it is we are actually deleting. + else + { + $this->ness['deleting'] = true; + + // If the node is a parent then we are going to delete all the children along with it. + // To delete a node without its children be sure to abandon the children before deleting. + if($this->parent()) + { + static::where('lft', 'BETWEEN', DB::raw($this->lft . ' AND ' . $this->rgt))->delete(); + } + + // If it's not a parent node then just delete the node, simple really! + else + { + $this->ness['deleting'] = false; + + // Allow Eloquent to delete the actual node. + parent::delete(); + } + + // Now to re-adjust the tree removing the gap we have left. + $this->adjustment($this->lft, ($this->width() + 1) * -1); + } + } + + return $this; + } + + /** + * Return an array of crumbs that led to the current node. + * + * + * $node = Ness::find(3); + * + * $crumbs = $node->crumbs(); + * + * + * @return array + */ + public function crumbs() + { + if(is_null($this->ness['crumbs'])) + { + $this->ness['crumbs'] = static::where('lft', '<', $this->lft)->where('rgt', '>', $this->rgt)->order_by('lft', 'asc')->get(); + } + + return $this->ness['crumbs']; + } + + /** + * Return an array of children for a node. + * + * + * $node = Ness::find(3); + * + * $children = $node->children(); + * + * + * @return array + */ + public function children() + { + if(!$this->parent()) + { + return $children = array(); + } + else + { + $sql = "SELECT node.*, (COUNT( parent.name ) -1) AS depth + FROM places AS node + CROSS JOIN places AS parent + WHERE node.lft > {$this->lft} + AND node.rgt < {$this->rgt} + AND node.lft BETWEEN parent.lft AND parent.rgt + GROUP BY node.id + ORDER BY node.lft"; + + $children = array(); + + foreach(DB::connection(static::$connection)->query($sql) as $object) + { + $children[] = new static((array) $object, true); + } + } + + return $children; + } + + /** + * Refreshes the node by getting the latest left and right values from the database. + * + * @return object + */ + public function refresh() + { + $updated = static::find($this->id); + + $this->fill(array( + 'lft' => $updated->lft, + 'rgt' => $updated->rgt + )); + + return $this; + } + + /** + * Fakes the removal of the node and its children from the database so that a node + * can be shifted around. + * + * @return object + */ + protected function fake() + { + // To fake the removal we'll shift the node outside of the tree by subtracting the + // right value from the node and its children. + static::where('lft', 'BETWEEN', DB::raw("{$this->lft} AND {$this->rgt}"))->update(array( + 'lft' => DB::raw("lft - {$this->rgt}"), + 'rgt' => DB::raw("rgt - {$this->rgt}") + )); + + // Adjust all those from our current left value by reducing a gap. Any nodes that have + // left and right values greater than or equal to this node will have 2 values taken + // from both their left and right values, thus 'removing' this node from the tree. + return $this->adjustment($this->lft, ($this->width() + 1) * -1); + } + + /** + * Revives a fake death (weird) node and all its children. Hallelujah! + * + * @param int $on + * @return object + */ + protected function revive($on) + { + // Make the adjustment to fit the node back into the tree. + $this->adjustment($on); + + // To revive the faked death we need to shift the node back into the tree by adding the + // left value and width of the node to those nodes between the negative width and zero. + $left = $this->width() * -1; + + $adjustment = $on + $this->width() + 1; + + static::where('lft', 'BETWEEN', DB::raw("{$left} AND 0"))->update(array( + 'lft' => DB::raw("lft + {$adjustment}"), + 'rgt' => DB::raw("rgt + {$adjustment}") + )); + + return $this; + } + + /** + * Perform an adjustment of all nodes from a given amount and of a given width. + * + * @param int $from + * @param int $width + * @return object + */ + protected function adjustment($from, $width = null) + { + if(is_null($width)) + { + $width = $this->width() + 1; + } + + static::where('lft', '>', $from)->update(array('lft' => DB::raw("lft + {$width}"))); + + static::where('rgt', '>', $from)->update(array('rgt' => DB::raw("rgt + {$width}"))); + + return $this; + } + + /** + * A node is a parent when its width is greater than one. + * + * + * if($node->parent()) + * { + * // The node is a parent. + * } + * + * + * @return bool + */ + public function parent() + { + return $this->width() > 1; + } + + /** + * Returns the width of the node in the tree. + * + * @return int + */ + public function width() + { + return $this->rgt - $this->lft; + } + + /** + * Checks a node to make sure it's valid, if not attempts to make it valid. + * + * @param object|int $node + * @return object + */ + protected function check($node) + { + if(is_numeric($node)) + { + $node = static::find($node); + } + + return $node; + } + + /** + * Overload the Eloquent save method so we can set the left and right values of a node if they aren't + * set by one of the other methods. + * + * @return bool + */ + public function save() + { + if(!$this->exists and is_null($this->lft)) + { + // If this is the first node in the tree then it gets the left and right values of 1 and 2. + if(!$after = static::select(array('lft', 'rgt'))->order_by('rgt', 'desc')->take(1)->first()) + { + $this->fill(array( + 'lft' => 1, + 'rgt' => 2 + )); + } + else + { + $this->fill(array( + 'lft' => $after->rgt + 1, + 'rgt' => $after->rgt + 2 + )); + } + + } + + return parent::save(); + } + +} \ No newline at end of file diff --git a/applications/core/models/permission.php b/applications/core/models/permission.php new file mode 100644 index 0000000..0d6c46c --- /dev/null +++ b/applications/core/models/permission.php @@ -0,0 +1,31 @@ +belongs_to('Feather\\Core\\Permission\\Profile', 'profile_id'); + } + +} \ No newline at end of file diff --git a/applications/core/models/permission/profile.php b/applications/core/models/permission/profile.php new file mode 100644 index 0000000..482b4a4 --- /dev/null +++ b/applications/core/models/permission/profile.php @@ -0,0 +1,24 @@ +has_many('Feather\\Admin\\Core\\Permission', 'profile_id'); + } + +} \ No newline at end of file diff --git a/applications/core/models/place.php b/applications/core/models/place.php new file mode 100644 index 0000000..25c370b --- /dev/null +++ b/applications/core/models/place.php @@ -0,0 +1,552 @@ +has_many('Feather\\Core\\Permission', 'place_id'); + } + + /** + * A place has many moderators. + * + * @return object + */ + public function moderators() + { + return $this->has_many('Feather\\Core\\Place\\Moderator', 'place_id'); + } + + /** + * A place has many discussions. + * + * @return object + */ + public function discussions() + { + return $this->has_many('Feather\\Core\\Discussion', 'place_id'); + } + + /** + * Returns all enriched places. + * + * @return array + */ + public static function all() + { + return Cache::remember('places', function() + { + $table = DB::connection(FEATHER_DATABASE)->config['prefix'] . Place::$table; + + $sql = "SELECT node.*, (COUNT( parent.name ) -1) AS depth + FROM {$table} AS node + CROSS JOIN places AS parent + WHERE node.lft BETWEEN parent.lft + AND parent.rgt + GROUP BY node.id + ORDER BY node.lft"; + + return Place::enrichment(DB::connection(FEATHER_DATABASE)->query($sql)); + }, static::cache_time); + } + + /** + * Select only one place and limit the number of discussions per page. + * + * @param int $id + * @param int $discussions_per_page + * @return Feather\Core\Place + */ + public static function one($id, $discussions_per_page = 20) + { + $place = Cache::remember("place_{$id}", function() use ($id) + { + $table = DB::connection(FEATHER_DATABASE)->config['prefix'] . Place::$table; + + $sql = "SELECT node.*, (COUNT( parent.name ) -1) AS depth + FROM {$table} AS node + CROSS JOIN places AS parent + WHERE node.id = {$id} + AND node.lft BETWEEN parent.lft + AND parent.rgt + GROUP BY node.id + ORDER BY node.lft"; + + $place = Place::enrichment(DB::connection(FEATHER_DATABASE)->query($sql)); + + return reset($place); + }, static::cache_time); + + $places = static::filter_by_permissions(array($place->id => $place) + static::enrichment($place->children())); + + list($sorted, $pivot) = static::sort($places); + + // Tally the total number of discussions within each place. + $totals = static::tally_discussions($places); + + $page = Paginator::page($total_results = array_sum($totals), $discussions_per_page); + + $skip = ($page - 1) * $discussions_per_page; + + // With our paginator variables we can now select only the discussions we want from the database. + $discussions = !$places ? array() : Discussion::where_in('place_id', array_keys($places)) + ->order_by('updated_at', 'desc') + ->skip($skip) + ->take($discussions_per_page) + ->get(); + + if($discussions) + { + $discussions = Discussion::enrichment($discussions, array('author', 'recent', 'participants')); + } + + $places = static::refine($discussions, $places, $sorted, $pivot, $totals, $discussions_per_page); + + if(empty($places)) + { + return array(); + } + + $place = reset($places); + + $pagination = Paginator::make($place->discussions, $total_results, $discussions_per_page); + + return $place->fill(array( + 'discussions' => $pagination->results, + 'pagination' => $pagination->links() + )); + } + + /** + * Returns an array of places for the index page of Feather. Places will receive a set amount of + * newest discussions from any of the places within the root place. + * + * @param int $discussions_per_place + * @return array + */ + public static function index($discussions_per_place) + { + $places = static::filter_by_permissions(static::all()); + + list($sorted, $pivot) = static::sort($places); + + // Spin through the sorted places and build an array of all the IDs including the child places. + // We'll then fetch the discussions for each place. This isn't exactly ideal but generally there + // shouldn't be a whole lot of root level places. + $discussions = array(); + + foreach($sorted as $place) + { + $ids = (array) $place->id; + + foreach($place->children as $child) $ids[] = $child->id; + + foreach(Discussion::enrichment(Discussion::where_in('place_id', $ids)->order_by('updated_at', 'desc')->take($discussions_per_place * 3)->get()) as $discussion) + { + $discussions[] = $discussion; + } + } + + // Tally the total number of discussions within each place. + $totals = static::tally_discussions($places); + + // Returns a refined array consisting of all places with the correct discussions + // and totals assigned to it. + return static::refine($discussions, $places, $sorted, $pivot, $totals, $discussions_per_place); + } + + /** + * Filters discussions into the correct parent place and tallies up the remaining discussions for + * each. + * + * @param array $discussions + * @param array $places + * @param array $sorted + * @param array $intermediate + * @param string $limit + * @param int $totals + * @return array + */ + protected static function refine($discussions, $places, $sorted, $pivot, $totals, $limit) + { + // Discussions need to be ordered by their updated_at datetime field and then placed into their + // respective ancestors array. + $ordered = array(); + + foreach($discussions as $discussion) + { + if(Auth::cannot('view: discussion', $discussion)) + { + continue; + } + + $discussion->place = $places[$discussion->place_id]; + + // If the discussions place is a child then we need to use it's parent ID. + $id = isset($discussion->place->parent_id) ? $discussion->place->parent_id : $discussion->place->id; + + $ordered[$id]["{$discussion->updated_at} {$discussion->id}"] = $discussion; + + // Re-order the ordered discussions by key and in reverse. Newest first. + krsort($ordered[$id]); + } + + // Loop over the ordered results and add the latest discussions to each until + // the limit is reached. + $filtered = array(); + + foreach($ordered as $id => $discussions) + { + foreach($discussions as $discussion) + { + if(!array_key_exists($id, $filtered) or count($filtered[$id]) < $limit) + { + $filtered[$id][] = $discussion; + } + else + { + break; + } + } + } + + // Loop over the places array and tally up the total discussions for each place. + foreach($places as $place) + { + // If the discussions place is a child then we need to use it's parent ID. + $id = isset($place->parent_id) ? $place->parent_id : $place->id; + + // Update the discussion counter for the root level place. + $sorted[$pivot[$id]]->total->discussions += $totals[$place->id]; + + // To achieve a cascading discussion count, the child and all its ancestors also + // get the places discussions added to their total. + foreach($sorted[$pivot[$id]]->children as $key => $child) + { + if($place->id == $child->id or ($place->lft > $child->lft and $place->rgt < $child->rgt)) + { + $sorted[$pivot[$id]]->relationships['children'][$key]->total->discussions += $totals[$place->id]; + } + } + } + + foreach($sorted as $place) + { + $place->discussions = array(); + + if(array_key_exists($place->id, $filtered)) + { + $place->discussions = $filtered[$place->id]; + + $place->total->remaining = $place->total->discussions - count($filtered[$place->id]); + + if($place->total->remaining < 0) + { + $place->total->remaining = 0; + } + } + } + + return $sorted; + } + + /** + * Count the total number of discussions for each place. + * + * @param array $places + * @return array + */ + protected static function tally_discussions($places) + { + // Creates an array where the keys are the place IDs and the value for each is 0. + $totals = array_fill_keys(array_keys(array_flip(array_keys($places))), 0); + + if(empty($places)) return $totals; + + $aggregates = Discussion::select(array('place_id', DB::raw('COUNT(*) as aggregate'))) + ->where_in('place_id', array_keys($places)) + ->where_draft(0) + ->group_by('place_id') + ->get(); + + foreach($aggregates as $aggregate) + { + $totals[$aggregate->place_id] = $aggregate->aggregate; + } + + return $totals; + } + + /** + * Takes an array of places and transforms it into a multi-dimensional array of places + * sorted into their greatest grand parent. + * + * @param array $places + * @return array + */ + protected static function sort($places) + { + $sorted = $pivot = array(); + + // The gauge is set when the first parent is encountered. Children will then be sorted if their + // depth is greater than that of the gauge. + $gauge = null; + + foreach($places as $place) + { + if(is_null($gauge) or $place->depth == $gauge) + { + $sorted[] = $place; + + $pivot[$place->id] = count($sorted) - 1; + + $gauge = $place->depth; + } + else + { + $place->parent_id = $sorted[count($sorted) - 1]->id; + + $sorted[count($sorted) - 1]->relationships['children'][] = $place; + } + } + + return array($sorted, $pivot); + } + + /** + * Filters out places a user cannot view due to permissions. + * + * @param array $places + * @return array + */ + protected static function filter_by_permissions($places) + { + // Store the previous ID here so that we can check if the last place was a parent, + // and if we remove a child we'll check to see if it is no longer a parent. + $previous = null; + + // If a childs parent is not visible to a user then the child is also deemed + // not visible. + $until = null; + + // Make sure the user has the correct permissions to view the places. + foreach((array) $places as $place) + { + if(Auth::cannot('view: place', $place) or $place->rgt < $until) + { + // If the previous item is a parent then we'll subtract its right value until + // it no longer becomes a parent. + if($previous and $places[$previous]->parent) + { + $places[$previous]->rgt -= ($place->rgt - $place->lft) + 1; + + if($places[$previous]->rgt - $places[$previous]->lft <= 1) + { + $places[$previous]->parent = false; + } + } + + // If this place is a parent then we remove all of its children as well. + if($place->parent) + { + $until = $place->rgt; + } + + unset($places[$place->id]); + + continue; + } + + $previous = $place->id; + } + + + return $places; + } + + /** + * Enriches an array of places with some extremely useful properties that are used throughout + * the system. + * + * @param array $places + * @param string $selected + * @return array + */ + protected static function enrichment($places, $selected = null) + { + $enriched = array(); + + foreach($places as $place) + { + if(!$place instanceof Place) + { + $place = new static((array) $place, true); + } + + $place->attributes = array_merge($place->attributes, array( + 'parent' => ($place->rgt - $place->lft) > 1, + 'child' => ($place->depth > 0), + 'selected' => ($place->slug == $selected), + 'total' => (object) array('discussions' => 0, 'remaining' => 0) + )); + + $place->relationships = array_merge($place->relationships, array( + 'children' => array(), + 'moderators' => array(), + 'permissions' => array() + )); + + // Setting the original to be the same as the attributes prevents all the special properties from + // being seen as dirty and thus saved when we save a place. + $place->original = $place->attributes; + + $enriched[$place->id] = $place; + } + + if($enriched) + { + // Fetch all of the permissions for all of the places. Once we have the permissions we'll + // loop over all the permissions and assign them to their correct place. + $permissions = Permission::where_in('place_id', array_keys($enriched))->get(); + + foreach($permissions as $permission) + { + $enriched[$permission->place_id]->relationships['permissions'][$permission->id] = $permission; + } + + unset($permissions); + + // We'll do the same with the moderators as we did with the permissions. Find them all then assign + // them to the correct place. + $moderators = Place\Moderator::with('details')->where_in('place_id', array_keys($enriched))->get(); + + foreach($moderators as $moderator) + { + $enriched[$moderator->place_id]->relationships['moderators'][$moderator->id] = $moderator; + } + + unset($moderators); + } + + return $enriched; + } + + /** + * Returns an HTML select box options list. + * + * @param array $parameters + * @return array + */ + public static function options($parameters = array()) + { + // Build a default parameters array. I decided to use an array here because a whole bunch of + // parameters is just unwieldly. This is somewhat nicer. + $parameters = (object) array_merge(array( + 'places' => null, + 'disable' => array(), + 'postable' => false, + 'cascade' => true, + 'selected' => null, + 'padder' => '---', + 'permissions' => false, + 'action' => null + ), $parameters); + + if(is_null($parameters->places)) + { + $parameters->places = static::all(); + } + + // The disabling variable will store a currently being disabled place. If we are cascading the + // disabling, that is, disabling a places children as well, then we'll use this variable to check + // if the places are children. + $disabling = null; + + // The hiding variable is the same as the disabling variable except it's permission based. If a + // user cannot view a place then all children of that place will be hidden as well. + $hiding = null; + + $attributes = array( + 'disabled' => ' disabled="disabled"', + 'selected' => ' selected="selected"' + ); + + $callback = function($place) use ($parameters) + { + return str_pad(" {$place->name}", ($place->depth * strlen($parameters->padder)) + strlen($place->name), $parameters->padder, STR_PAD_LEFT); + }; + + $options = array(); + + foreach($parameters->places as $key => $place) + { + $disabled = $parameters->postable and !$place->postable ? $attributes['disabled'] : ''; + $selected = $place->id == $parameters->selected ? $attributes['selected'] : ''; + + // If the place is to be disabled then set the disabled attribute. Also sets the cascading + // for children if it's enabled. We also check if an action has been passed in and if so run + // that through our access control. This is handy for disabling places when starting a discussion. + if(in_array($place->id, (array) $parameters->disable) or ($parameters->action and Auth::cannot($parameters->action, $place))) + { + if($parameters->cascade) + { + $disabling = $place; + } + + $disabled = $attributes['disabled']; + } + + // Otherwise if the diasbling variable is an object check to see if the current place is a + // child of the originally disabled place, if it is then disable it as well. + elseif(is_object($disabling) and $place->lft > $disabling->lft and $place->rgt < $disabling->rgt) + { + $disabled = $attributes['disabled']; + } + + // If the hiding variable is an object check to see if the current place is a child of the + // originally hidden place, if it is hide the child as well. + elseif(is_object($hiding) and $place->lft > $hiding->lft and $place->rgt < $hiding->rgt) + { + continue; + } + + // If the user cannot view this place then we'll hide it, as well as that, we'll hide + // any children of this place. + if($parameters->permissions and Auth::cannot('view: place', $place)) + { + $hiding = $place; + + continue; + } + + $options[] = ''; + } + + return implode(PHP_EOL, $options); + } + +} \ No newline at end of file diff --git a/applications/core/models/place/moderator.php b/applications/core/models/place/moderator.php new file mode 100644 index 0000000..18a5295 --- /dev/null +++ b/applications/core/models/place/moderator.php @@ -0,0 +1,34 @@ +belongs_to('Feather\\Core\\User', 'user_id'); + } + + /** + * A moderator belongs to a place. + * + * @return object + */ + public function place() + { + return $this->belongs_to('Feather\\Core\\Place', 'place_id'); + } + +} \ No newline at end of file diff --git a/applications/core/models/reply.php b/applications/core/models/reply.php new file mode 100644 index 0000000..48fc724 --- /dev/null +++ b/applications/core/models/reply.php @@ -0,0 +1,48 @@ +has_one('Feather\\Core\\User', 'user_id'); + } + + /** + * A reply may have an editor. + * + * @return object + */ + public function editor() + { + return $this->has_one('Feather\\Core\\User', 'edited_by'); + } + + /** + * A reply may have a deleter. + * + * @return object + */ + public function deleter() + { + return $this->has_one('Feather\\Core\\User', 'deleted_by'); + } +} \ No newline at end of file diff --git a/applications/core/models/role.php b/applications/core/models/role.php new file mode 100644 index 0000000..48751dc --- /dev/null +++ b/applications/core/models/role.php @@ -0,0 +1,12 @@ +getKey(); - } - - /** - * Get the password for user authentication. - * - * @return string - */ - public function getAuthPassword() - { - return $this->password; - } + public static $timestamps = true; /** * A user can have many and belong to many roles. @@ -47,7 +27,7 @@ public function getAuthPassword() */ public function roles() { - return $this->belongsToMany('Feather\\Core\\Role', 'user_roles', 'user_id', 'role_id'); + return $this->has_many_and_belongs_to('Feather\\Core\\Role', 'user_roles', 'user_id', 'role_id'); } /** @@ -56,9 +36,9 @@ public function roles() * @param string $password * @return void */ - public function setPassword($password) + public function set_password($password) { - $this->setAttribute('password', Hash::make($password)); + $this->set_attribute('password', Hash::make($password)); } /** @@ -66,9 +46,9 @@ public function setPassword($password) * * @return string */ - public function getSlug() + public function get_slug() { - return Str::slug($this->getAttribute('username')); + return Str::slug($this->get_attribute('username')); } /** @@ -76,9 +56,9 @@ public function getSlug() * * @return string */ - public function getName() + public function get_name() { - return $this->getAttribute('username'); + return $this->get_attribute('username'); } /** @@ -86,9 +66,9 @@ public function getName() * * @return string */ - public function getAvatar() + public function get_avatar() { - return 'http://www.gravatar.com/avatar/' . md5(strtolower(trim($this->getAttribute('email')))); + return 'http://www.gravatar.com/avatar/' . md5(strtolower(trim($this->get_attribute('email')))); } /** @@ -98,9 +78,9 @@ public function getAvatar() * @param bool $activated * @return Feather\Core\User */ - public function register($input) + public static function register($input) { - $this->fill(array( + $user = new static(array( 'username' => $input['username'], 'password' => $input['password'], 'email' => $input['email'] @@ -108,13 +88,13 @@ public function register($input) if(Config::get('feather: db.registration.confirm_email')) { - $this->fill(array( + $user->fill(array( 'activation_key' => Str::random(30), 'activated' => 0 )); } - if(!$this->save()) + if(!$user->save()) { throw new FeatherModelException; } @@ -122,9 +102,9 @@ public function register($input) // Attach the users role. If the user needs to confirm their e-mail address they are given // the confirming role status which has an ID of 4. If not then they receive the standard // member role which has an ID of 2. - $this->roles()->attach(Config::get('feather: db.registration.confirm_email') ? 4 : 2); + $user->roles()->attach(Config::get('feather: db.registration.confirm_email') ? 4 : 2); - return $this; + return $user; } public static function edit($user, $input) diff --git a/applications/core/models/user/role.php b/applications/core/models/user/role.php new file mode 100644 index 0000000..45aa141 --- /dev/null +++ b/applications/core/models/user/role.php @@ -0,0 +1,14 @@ + 'feather', 'uses' => 'feather core::index@index')); + +/* +|-------------------------------------------------------------------------- +| User Authentication +|-------------------------------------------------------------------------- +*/ + +Route::any('(:bundle)/login', array('as' => 'login', 'uses' => 'feather core::index@login')); +Route::any('(:bundle)/register', array('as' => 'register', 'uses' => 'feather core::index@register')); +Route::get('(:bundle)/logout', array('as' => 'logout', 'uses' => 'feather core::index@logout')); + +/* +|-------------------------------------------------------------------------- +| Members and Profiles +|-------------------------------------------------------------------------- +*/ + +Route::get('(:bundle)/members', array('as' => 'members', 'uses' => 'feather core::members@index')); +Route::get('(:bundle)/profile/(:any)', array('as' => 'profile', 'uses' => 'feather core::profile@index')); + +/* +|-------------------------------------------------------------------------- +| Places +|-------------------------------------------------------------------------- +*/ + +Route::get('(:bundle)/place/(:num)-(:any)/(:page?)', array('as' => 'place', 'uses' => 'feather core::place@index')); + +/* +|-------------------------------------------------------------------------- +| Discussions and Starting Discussions +|-------------------------------------------------------------------------- +*/ + +Route::any('(:bundle)/discussion/start', array('as' => 'start.discussion', 'uses' => 'feather core::discussion@start')); +Route::any('(:bundle)/place/(:num)-(:any)/start', 'feather core::discussion@start'); +Route::get('(:bundle)/discussion/(:num)-(:any)/latest', array('as' => 'discussion.latest', 'uses' => 'feather core::discussion@index')); +Route::any('(:bundle)/discussion/(:num)-(:any)/(:page?)', array('as' => 'discussion', 'uses' => 'feather core::discussion@index')); +Route::any('(:bundle)/discussion/(:num)-(:any)/edit', array('as' => 'discussion.edit', 'uses' => 'feather core::discussion@edit')); +Route::post('(:bundle)/discussion/preview', 'feather core::discussion@preview'); + +/* +|-------------------------------------------------------------------------- +| Rules +|-------------------------------------------------------------------------- +*/ + +Route::get('(:bundle)/rules', array('as' => 'rules', 'uses' => 'feather core::index@rules')); + +/* +|-------------------------------------------------------------------------- +| API Routes +|-------------------------------------------------------------------------- +*/ + +Route::get('(:bundle)/api/(:any)/(:any).(json)', 'feather core::api.(:1).(:2)@index'); +Route::get('(:bundle)/api/(:any)/(:any)/(:any).(json)', 'feather core::api.(:1).(:2)@(:3)'); + +/* +|-------------------------------------------------------------------------- +| Feather Catch-all +|-------------------------------------------------------------------------- +| +| This catch-all route allows plugins to create new controllers and actions +| on the fly. +| +*/ + +Route::get('(:bundle)/(:all)', function($parameters) +{ + $parameters = explode('/', $parameters); + + // Update the Request::$route property so that it contains the controller + // and controller_action properties. + Request::$route->controller = array_shift($parameters); + + Request::$route->controller_action = empty($parameters) ? 'index' : array_shift($parameters); + + return IoC::resolve('controller: base')->execute(Request::$route->controller_action, $parameters); +}); \ No newline at end of file diff --git a/applications/core/start.php b/applications/core/start.php new file mode 100644 index 0000000..1177091 --- /dev/null +++ b/applications/core/start.php @@ -0,0 +1,40 @@ + path('core') . 'controllers' . DS . 'api' . EXT, + 'Feather_Base_Controller' => path('core') . 'controllers' . DS . 'base' . EXT +)); + +/* +|-------------------------------------------------------------------------- +| Core Template +|-------------------------------------------------------------------------- +| +| Name the core template. +| +*/ + +View::name('feather core::template', 'template'); \ No newline at end of file diff --git a/applications/core/validation/auth.php b/applications/core/validation/auth.php new file mode 100644 index 0000000..6adbc70 --- /dev/null +++ b/applications/core/validation/auth.php @@ -0,0 +1,68 @@ + function($validator) + { + $validator->rule('username', 'required') + ->rule('password', 'required') + ->message('username.required', 'login.messages.username.is_required') + ->message('password.required', 'login.messages.password.is_required'); + }, + + 'register' => function($validator, $feather) + { + $validator->rule('username', array('required', 'min:3', 'max:20', 'alpha_dot', 'unique:users,username')) + ->rule('password', array('required', 'min:7', 'max:15', 'confirmed')) + ->rule('password_confirmation', array('required')) + ->rule('email', array('required', 'email', 'unique:users,email')) + ->message('username.required', 'register.messages.username.is_required') + ->message('username.min', array('register.messages.username.too_short', array('length' => 3))) + ->message('username.max', array('register.messages.username.too_long', array('length' => 20))) + ->message('username.alpha_dot', 'register.messages.username.is_invalid') + ->message('username.unique', 'register.messages.username.already_exists') + ->message('password.required', 'register.messages.password.is_required') + ->message('password.min', array('register.messages.password.too_short', array('length' => 7))) + ->message('password.max', array('register.messages.password.too_long', array('length' => 15))) + ->message('password.confirmed', 'register.messages.password.did_not_match') + ->message('password_confirmation.required', 'register.messages.password_confirmation.is_required') + ->message('email.required', 'register.messages.email.is_required') + ->message('email.email', 'register.messages.email.invalid') + ->message('email.unique', 'register.messages.email.already_exists'); + + if($feather['config']->get('feather: db.registration.rules')) + { + $validator->rule('rules', 'accepted') + ->message('rules.accepted', 'register.messages.rules.not_accepted'); + } + }, + + 'sso' => array( + + 'create' => function($validator) + { + $validator->rule('create_username', array('required', 'min:3', 'max:20', 'alpha_dot', 'unique:users,username')) + ->rule('create_email', array('required', 'email', 'unique:users,email')) + ->message('create_username.required', 'authenticate.errors.register.username.is_required') + ->message('create_username.min', array('authenticate.errors.register.username.too_short', array('length' => 3))) + ->message('create_username.max', array('authenticate.errors.register.username.too_long', array('length' => 20))) + ->message('create_username.alpha_dot', 'authenticate.errors.register.username.is_invalid') + ->message('create_username.unique', 'authenticate.errors.register.username.already_exists') + ->message('create_email.required', 'authenticate.errors.register.email.is_required') + ->message('create_email.email', 'authenticate.errors.register.email.invalid') + ->message('create_email.unique', 'authenticate.errors.register.email.already_exists'); + }, + + 'connect' => function($validator) + { + $validator->rule('connect_email', array('required', 'email')) + ->rule('connect_password', array('required', 'min:7', 'max:15')) + ->message('connect_email.required', 'authenticate.errors.register.email.is_required') + ->message('connect_email.unique', 'authenticate.errors.register.email.already_exists') + ->message('connect_password.required', 'authenticate.errors.register.password.is_required') + ->message('connect_password.min', array('authenticate.errors.register.password.too_short', array('length' => 7))) + ->message('connect_password.max', array('authenticate.errors.register.password.too_long', array('length' => 15))); + } + + ) +); \ No newline at end of file diff --git a/applications/core/validation/discussion.php b/applications/core/validation/discussion.php new file mode 100644 index 0000000..940b579 --- /dev/null +++ b/applications/core/validation/discussion.php @@ -0,0 +1,33 @@ + function($validator) + { + $validator->rule('body', 'required') + ->rule('place', 'required') + ->message('body.required', 'discussion.messages.body.is_required') + ->message('place.required', 'discussion.messages.place.is_required'); + + if(isset($validator->input['start'])) + { + $validator->rule('title', 'required') + ->message('title.required', 'discussion.messages.title.is_required'); + } + }, + + 'edit' => function($validator) + { + $validator->rule('body', 'required') + ->rule('place', 'required') + ->message('body.required', 'discussion.messages.body.is_required') + ->message('place.required', 'discussion.messages.place.is_required'); + + if(isset($validator->input['start']) or isset($validator->input['draft'])) + { + $validator->rule('title', 'required') + ->message('title.required', 'discussion.messages.title.is_required'); + } + } + +); \ No newline at end of file diff --git a/applications/core/views/discussion/edit.blade.php b/applications/core/views/discussion/edit.blade.php new file mode 100644 index 0000000..3da4eae --- /dev/null +++ b/applications/core/views/discussion/edit.blade.php @@ -0,0 +1,90 @@ +{{ $preview }} + +

{{ __('feather core::titles.edit_discussion', array('discussion' => $discussion->title)) }}

+ +
+ {{ Form::open(null, 'put') }} + +
+ +
+ +
{{ Form::label('place', __('feather core::discussion.labels.place.title')) }}
+
+ + +
+ {{ __('feather core::discussion.labels.place.helper') }} +
+ + @error('place') +
+ + @if($discussion->draft) +
{{ Form::label('title', __('feather core::discussion.labels.title.title')) }}
+
+ {{ Form::text('title', Input::old('title', $discussion->title)) }} + + @error('title') +
+ @endif + +
{{ Form::label('participants', __('feather core::discussion.labels.participants.title')) }}
+
+
+ {{ Form::text('participants', Input::old('participants', $participants), array('autocomplete' => 'off')) }} + +
+
+ +
+ {{ __('feather core::discussion.labels.participants.helper') }} +
+ +
+ {{ __('feather core::discussion.anyone_can_participate') }} +
+
+ +
+ +
+ @event('view: before discussion.body') + + {{ Form::textarea('body', Input::old('body', $discussion->body)) }} + + @event('view: after discussion.body') +
+ + @error('body') + +
+ +
+
+
+ + @if($discussion->draft) + + {{ Form::submit(__('feather core::discussion.start_discussion'), array('name' => 'start', 'class' => 'btn btn-primary btn-big')) }} + + {{ Form::submit(__('feather core::discussion.save_draft'), array('name' => 'draft', 'class' => 'btn btn-inverse tooltip-ui-right', 'title' => __('feather core::discussion.labels.save_draft.helper'))) }} + + @else + + {{ Form::submit(__('feather core::discussion.save_discussion'), array('name' => 'save', 'class' => 'btn btn-primary btn-big')) }} + + @endif + +
+ +
+ {{ Form::submit(__('feather core::discussion.preview_discussion'), array('name' => 'preview', 'class' => 'btn btn-inverse')) }} +
+
+
+ + {{ Form::token() . Form::close() }} +
\ No newline at end of file diff --git a/applications/core/views/discussion/preview.blade.php b/applications/core/views/discussion/preview.blade.php new file mode 100644 index 0000000..0d949e0 --- /dev/null +++ b/applications/core/views/discussion/preview.blade.php @@ -0,0 +1,5 @@ +

Discussion Preview

+ +
+ {{ $body }} +
\ No newline at end of file diff --git a/applications/core/views/discussion/start.blade.php b/applications/core/views/discussion/start.blade.php new file mode 100644 index 0000000..3df3f4a --- /dev/null +++ b/applications/core/views/discussion/start.blade.php @@ -0,0 +1,78 @@ +{{ $preview }} + +

{{ __('feather core::titles.start_discussion') }}

+ +
+ {{ Form::open(null, 'post') }} + +
+ +
+ +
{{ Form::label('place', __('feather core::discussion.labels.place.title')) }}
+
+ + +
+ {{ __('feather core::discussion.labels.place.helper') }} +
+ + @error('place') +
+ +
{{ Form::label('title', __('feather core::discussion.labels.title.title')) }}
+
+ {{ Form::text('title', Input::old('title')) }} + + @error('title') +
+ +
{{ Form::label('participants', __('feather core::discussion.labels.participants.title')) }}
+
+
+ {{ Form::text('participants', Input::old('participants'), array('autocomplete' => 'off')) }} + +
+
+ +
+ {{ __('feather core::discussion.labels.participants.helper') }} +
+ +
+ {{ __('feather core::discussion.anyone_can_participate') }} +
+
+ +
+ +
+ @event('view: before discussion.body') + + {{ Form::textarea('body', Input::old('body')) }} + + @event('view: after discussion.body') +
+ + @error('body') + +
+ +
+
+
+ {{ Form::submit(__('feather core::discussion.start_discussion'), array('name' => 'start', 'class' => 'btn btn-primary btn-big')) }} + {{ Form::submit(__('feather core::discussion.save_draft'), array('name' => 'draft', 'class' => 'btn btn-inverse tooltip-ui-right', 'title' => __('feather core::discussion.labels.save_draft.helper'))) }} +
+ +
+ {{ Form::submit(__('feather core::discussion.preview_discussion'), array('name' => 'preview', 'class' => 'btn btn-inverse')) }} +
+
+
+ + {{ Form::token() . Form::close() }} +
\ No newline at end of file diff --git a/applications/core/views/discussion/view.blade.php b/applications/core/views/discussion/view.blade.php new file mode 100644 index 0000000..d784ede --- /dev/null +++ b/applications/core/views/discussion/view.blade.php @@ -0,0 +1,13 @@ +

{{ $discussion->title }}

+ +
+ @foreach($discussion->replies as $reply) + +
+ + + +
+ + @endforeach +
\ No newline at end of file diff --git a/applications/core/views/error/inline.blade.php b/applications/core/views/error/inline.blade.php new file mode 100644 index 0000000..e7e6422 --- /dev/null +++ b/applications/core/views/error/inline.blade.php @@ -0,0 +1,2 @@ + +{{ $error }} \ No newline at end of file diff --git a/applications/core/views/error/page.blade.php b/applications/core/views/error/page.blade.php new file mode 100644 index 0000000..c2eb0ae --- /dev/null +++ b/applications/core/views/error/page.blade.php @@ -0,0 +1,7 @@ +

{{ $error['title'] }}

+ +
+ {{ $error['error'] }} +
+ +{{ HTML::Link_to_route('feather', '← Back to places') }} \ No newline at end of file diff --git a/applications/core/views/index/home.blade.php b/applications/core/views/index/home.blade.php new file mode 100644 index 0000000..ec091aa --- /dev/null +++ b/applications/core/views/index/home.blade.php @@ -0,0 +1,58 @@ +@if(empty($places)) + +

There's nothing here

+ +

+ You may not have the correct permissions to view any places or discussions. Or someone is a little lazy and hasn't created any yet. +

+@else + + + +@endif \ No newline at end of file diff --git a/applications/core/views/menu/guest.blade.php b/applications/core/views/menu/guest.blade.php new file mode 100644 index 0000000..a61f73a --- /dev/null +++ b/applications/core/views/menu/guest.blade.php @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/applications/core/views/menu/user.blade.php b/applications/core/views/menu/user.blade.php new file mode 100644 index 0000000..39c771c --- /dev/null +++ b/applications/core/views/menu/user.blade.php @@ -0,0 +1,33 @@ + \ No newline at end of file diff --git a/applications/core/views/misc/rules.blade.php b/applications/core/views/misc/rules.blade.php new file mode 100644 index 0000000..877c2ff --- /dev/null +++ b/applications/core/views/misc/rules.blade.php @@ -0,0 +1,3 @@ +

Community Rules

+ +{{ Feather\Gear\Markdown\Parse(Feather\Config::get('feather: db.registration.rules_message')) }} \ No newline at end of file diff --git a/applications/core/views/place/partial/children.blade.php b/applications/core/views/place/partial/children.blade.php new file mode 100644 index 0000000..4839036 --- /dev/null +++ b/applications/core/views/place/partial/children.blade.php @@ -0,0 +1,36 @@ +@if($place->parent) + + @assign($depth, 1) + + @assign($difference, 0 - $place->depth) + + ', $depth) }} +@endif \ No newline at end of file diff --git a/applications/core/views/place/partial/discussion.blade.php b/applications/core/views/place/partial/discussion.blade.php new file mode 100644 index 0000000..2a54ad7 --- /dev/null +++ b/applications/core/views/place/partial/discussion.blade.php @@ -0,0 +1,66 @@ +{{-- TODO: Make this theme related as it contains theme things, like tooltips. --}} +
  • +
    + {{ HTML::link_to_route('place', $discussion->place->name, array($discussion->place->id, $discussion->place->slug), array('title' => $discussion->place->description, 'class' => 'btn btn-soft tooltip-ui-places')) }} +
    + +
    + + @event('view.before: place.discussion.title') + + @if($discussion->private) + Private + @endif + + @if($discussion->draft) + Draft + @endif + + {{ HTML::link_to_route('discussion', $discussion->title, array($discussion->id, $discussion->slug, null), array('class' => 'title')) }} + + @event('view.after: place.discussion.title') + + @event('view.before: place.discussion.meta') + +

    + @if($discussion->recent) + Last reply by Dayle, on 23th June + @elseif($discussion->author) + Started by {{ HTML::link_to_route('profile', $discussion->author->username, array($discussion->author->slug)) }}, {{ Feather\Date::meta($discussion->created_at) }} + @endif +

    + +
    + @if(Feather\Auth::activated()) + {{ HTML::link('', '', array('class' => 'watch tooltip-ui', 'title' => 'Watch Discussion')) }} + @endif + + @if(Feather\Auth::is(array('admin', 'moderator'))) + {{ HTML::link('', '', array('class' => 'tools tooltip-ui', 'title' => 'Discussion Tools')) }} + @endif +
    + + @event('view.after: place.discussion.meta') +
    + +
    + + @event('view.before: place.discussion.stats') + +
    + Views + {{ $discussion->short_views }} +
    +
    + Replies + {{ $discussion->short_replies }} +
    +
    + Watchers + 0 +
    + + @event('view.after: place.discussion.stats') + +
    +
  • \ No newline at end of file diff --git a/applications/core/views/place/view.blade.php b/applications/core/views/place/view.blade.php new file mode 100644 index 0000000..eaf5e81 --- /dev/null +++ b/applications/core/views/place/view.blade.php @@ -0,0 +1,39 @@ +
    +

    + {{ HTML::link_to_route('place', $place->name, array($place->id, $place->slug)) }} + + {{ number_format($place->total->discussions) }} +

    + + @include('feather core::place.partial.children') + + @if($place->description) +
    + {{ $place->description }} +
    + @endif +
    + + \ No newline at end of file diff --git a/applications/core/views/user/login.blade.php b/applications/core/views/user/login.blade.php new file mode 100644 index 0000000..102b2d6 --- /dev/null +++ b/applications/core/views/user/login.blade.php @@ -0,0 +1,52 @@ +

    {{ __('feather core::titles.login') }}

    + +{{ Form::open(urldecode(URI::full()), 'post') }} + +
    + +
    + +
    {{ Form::label('username', __('feather core::common.username')) }}
    +
    + {{ Form::text('username', Input::old('username')) }} + + @error('username') +
    + +
    {{ Form::label('password', __('feather core::common.password')) }}
    +
    + {{ Form::password('password') }} + + @error('password') +
    + +
    {{ Form::label('remember', __('feather core::login.labels.remember.title')) }}
    +
    +
    + + +
    + +
    + {{ __('feather core::login.labels.remember.helper') }} +
    +
    + +
    + +
    + +
    + {{ Form::submit(__('feather core::common.login'), array('class' => 'btn btn-primary btn-big')) }}   + {{ HTML::link_to_route('register', __('feather core::login.labels.register.title'), null, array('class' => 'btn large')) }} +
    + +{{ Form::token() . Form::close() }} \ No newline at end of file diff --git a/applications/core/views/user/register.blade.php b/applications/core/views/user/register.blade.php new file mode 100644 index 0000000..c049922 --- /dev/null +++ b/applications/core/views/user/register.blade.php @@ -0,0 +1,82 @@ +

    {{ __('feather core::titles.register') }}

    + +{{ Form::open(URI::full(), 'post') }} + +
    + +
    + +
    + +
    + +
    {{ Form::label('username', __('feather core::common.username')) }}
    +
    + {{ Form::text('username', Input::old('username'), array('autocomplete' => 'off', 'tabindex' => 1)) }} + + @error('username') +
    + +
    {{ Form::label('email', __('feather core::common.email')) }}
    +
    + {{ Form::text('email', Input::old('email'), array('autocomplete' => 'off', 'tabindex' => 1)) }} + + @error('email') +
    + + @event('view: before register.rules') + + @if(Feather\Config::get('feather: db.registration.rules', 0)) + +
    +
    + + + @error('rules') +
    + + @endif + + @event('view: after register.rules') + +
    + +
    + +
    + +
    + +
    + +
    +
    {{ Form::label('password', __('feather core::common.password')) }}
    +
    + {{ Form::password('password', array('autocomplete' => 'off', 'tabindex' => 1)) }} + + @error('password') +
    + +
    {{ Form::label('password_confirmation', __('feather core::register.labels.password_confirmation.title')) }}
    +
    + {{ Form::password('password_confirmation', array('autocomplete' => 'off', 'tabindex' => 1)) }} + + @error('password_confirmation') +
    +
    + +
    + +
    + +
    + +
    + {{ Form::submit(__('feather core::common.register'), array('class' => 'btn btn-primary btn-big', 'tabindex' => 2)) }} +
    + +{{ Form::token() . Form::close() }} \ No newline at end of file diff --git a/artisan b/artisan deleted file mode 100644 index 9a5fabf..0000000 --- a/artisan +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env php -run(); diff --git a/bootstrap/exceptions.php b/bootstrap/exceptions.php new file mode 100644 index 0000000..c45d00d --- /dev/null +++ b/bootstrap/exceptions.php @@ -0,0 +1,69 @@ +errors = $errors; + } + + /** + * Get the errors. + * + * @return array + */ + public function get() + { + return $this->errors; + } + +} + +/* +|-------------------------------------------------------------------------- +| Feather Model Exception +|-------------------------------------------------------------------------- +| +| The exception class for Feather models. +| +*/ + +class FeatherModelException extends Exception {} + +/* +|-------------------------------------------------------------------------- +| Auth Exception +|-------------------------------------------------------------------------- +| +| The exception class for Feather's Authentication. +| +*/ + +class AuthException extends Exception {} \ No newline at end of file diff --git a/bootstrap/ioc.php b/bootstrap/ioc.php new file mode 100644 index 0000000..339940b --- /dev/null +++ b/bootstrap/ioc.php @@ -0,0 +1,31 @@ +bundle('feather/themes/' . $feather['config']->get('feather: db.forum.theme', 'default')); + +set_path('theme', path('themes') . $feather['config']->get('feather: db.forum.theme', 'default') . DS); + +Bundle::register('feather theme', array( + 'location' => 'path: ' . path('theme') +)); + +/* +|-------------------------------------------------------------------------- +| View Event Override +|-------------------------------------------------------------------------- +| +| Feather views behave differently to standard Laravel views. Templates +| have the ability to override views found in applications. This allows +| templates to provide custom structuring depending on the features the +| template offers. +| +| To enable this functionality Feather must override the event loader of +| view files. +| +*/ + +Event::override(View::loader, function($bundle, $view) +{ + if(!str_contains($bundle, ' ')) + { + $bundle = "feather {$bundle}"; + } + + $path = Bundle::path($bundle) . 'views'; + + if(!is_null(View::file($bundle, $view, path('theme') . 'views'))) + { + $path = path('theme') . 'views'; + } + + if(str_contains($view, ': ')) + { + list($directory, $view) = explode(': ', $view); + + list($name, $view) = explode(' ', $view); + + if(!is_null(View::file($bundle, $view, path('feather') . Str::plural($directory) . DS . $name . DS . 'views'))) + { + $path = path('feather') . Str::plural($directory) . DS . $name . DS . 'views'; + } + } + + return View::file($bundle, $view, $path); +}); + +/* +|-------------------------------------------------------------------------- +| Global Feather Variable +|-------------------------------------------------------------------------- +| +| The feather global view variable is shared across all views. It contains +| some basic information about the forum as well as the currently logged in +| user. +| +*/ + +Event::listen('auth: started', function() use ($feather) +{ + $user = array(); + + if($feather['auth']->online()) + { + $user = $feather['auth']->user(); + } + + View::share('feather', (object) array_merge($feather['config']->get('feather: db.forum'), array('user' => (object) $user))); +}); + +/* +|-------------------------------------------------------------------------- +| Default Variable Assignment +|-------------------------------------------------------------------------- +| +| Some variables are used almost always inside a template. To ensure these +| varaiables are always set we'll set them if they are not currently set. +| +*/ + +$defaults = function($view) +{ + if(!isset($view->title)) $view->title = 'Index'; + + if(!isset($view->alert)) $view->alert = Session::has('alert') ? (object) Session::get('alert') : null; +}; + +View::composer('feather core::template', $defaults); +View::composer('feather admin::template', $defaults); + +/* +|-------------------------------------------------------------------------- +| Blade Variable Assignment +|-------------------------------------------------------------------------- +| +| Assign variables within Blade. +| +*/ + +Blade::extend(function($value) +{ + return preg_replace('/(\s*)@assign\s*\(\$(.*), (.*)\)(\s*)/', '$1$4', $value); +}); + +/* +|-------------------------------------------------------------------------- +| Blade Events +|-------------------------------------------------------------------------- +| +| Fire custom Gear events. +| +*/ + +Blade::extend(function($value) +{ + $matcher = Blade::matcher('event'); + + return preg_replace($matcher, '$1', $value); +}); + +/* +|-------------------------------------------------------------------------- +| Blade Inline Errors +|-------------------------------------------------------------------------- +| +| Display inline errors for a specific form element. +| +*/ + +Blade::extend(function($value) +{ + $matcher = Blade::matcher('error'); + + return preg_replace($matcher, '$1has$2 ? view("feather core::error.inline", array("error" => $errors->first$2)) : null; ?>', $value); +}); + +/* +|-------------------------------------------------------------------------- +| Blade Errors +|-------------------------------------------------------------------------- +| +| Display all errors for a form. +| +*/ + +Blade::extend(function($value) +{ + $matcher = Blade::matcher('errors'); + + return preg_replace($matcher, '$1all() ? view("feather core::error.page", array("errors" => $errors->all())) : null; ?>', $value); +}); + +/* +|-------------------------------------------------------------------------- +| HTML::link_to_new_discussion() Macro +|-------------------------------------------------------------------------- +| +| Custom HTML macro to link to the new discussion page. +| +*/ + +HTML::macro('link_to_new_discussion', function($title, $attributes = array()) use ($feather) +{ + $uri = URI::current(); + + if(str_contains($uri, 'place')) + { + $url = preg_replace('/\/p[0-9]+/', '', $uri) . (ends_with($uri, 'start') ? null : '/start'); + + preg_match('/(\d+)-.*?/', $uri, $matches); + + // If the user cannot start discussions on the selected place, don't show the button. + if($feather['auth']->cannot('start: discussions', Core\Place::find(array_pop($matches)))) + { + $url = URL::to_route('start.discussion'); + } + } + else + { + $url = URL::to_route('start.discussion'); + } + + return HTML::link($url, $title, $attributes); +}); \ No newline at end of file diff --git a/components/auth/authorizer.php b/components/auth/authorizer.php new file mode 100644 index 0000000..27b2d5c --- /dev/null +++ b/components/auth/authorizer.php @@ -0,0 +1,257 @@ +feather['config']->set('laravel: auth.driver', 'feather'); + + $this->user = $this->user(); + + // Depending on the authenticator being used their may be a response thrown back that + // interupts the normal response of Feather. + $authenticator = $this->feather['config']->get('feather: db.auth.driver'); + + if(!$this->online() and ($response = $this->feather['gear']->first("auth: bootstrap {$authenticator}"))) + { + Route::filter('feather::before', function() use ($response) + { + return $response; + }); + } + + $this->feather['gear']->fire('auth: started'); + } + + /** + * Access control prevents users without specific permissions from accessing certain parts + * of Feather. Actions can have rules defined in the configuration file. This method will + * check a user to see if they have the correct permissions to perform the given action. + * + * @param string $action + * @param object $resource + * @return bool + */ + public function can($action, $resource = null) + { + if(!str_contains($action, ': ')) + { + throw new InvalidArgumentException("Invalid action [{$action}] supplied to ACL."); + } + + list($verb, $action) = explode(': ', $action); + + // To make things look lovely when writing actions we'll replace any spaces with underscores + // as that's how role permissions are stored in the database. + $action = str_replace(' ', '_', $action); + + foreach($this->user->roles as $role) + { + if($role->{"{$verb}_{$action}"}) + { + // If we have a valid resource then we'll need to see if we have a rule in the configuration + // for this action. If there is no rule then there is further checking that needs to be done + // on the resource. + if(!is_null($resource)) + { + if($this->feather['config']->has("feather: auth.rules.{$verb}.{$action}")) + { + $resolver = $this->feather['config']->get("feather: auth.rules.{$verb}.{$action}"); + + return $resolver($resource); + } + + continue; + } + + // If there was no valid resource then the action was simply a role based action. We + // can return true here because the user has the correct permissions for this action. + return true; + } + } + + // If we have a resource and it's an instance of Place then we need to perform a couple extra + // permission checks on it. + if($resource instanceof \Feather\Core\Place) + { + // Some places are not postable, make sure we aren't trying to start a discussion + // in a non-postable place. + if(in_array($verb, array('start')) and !$resource->postable) + { + return false; + } + + foreach($resource->permissions as $permission) + { + if(array_key_exists($permission->role_id, $this->user->roles)) + { + if($permission->{"{$verb}_{$action}"}) + { + return true; + } + } + } + } + + // Regardless, we need to run through the authentication rules in the configuration file. + else + { + if($this->feather['config']->has("feather: auth.rules.{$verb}.{$action}")) + { + $resolver = $this->feather['config']->get("feather: auth.rules.{$verb}.{$action}"); + + return $resolver($resource); + } + } + + // Sorry chum, but you can't do that! + return false; + } + + /** + * The complete opposite of above, determines if a user is not able to perform an action. + * + * @param string $action + * @param object $resource + * @return bool + */ + public function cannot($action, $resource = null) + { + return !$this->can($action, $resource); + } + + /** + * Determines if a user is a given role or roles. + * + * @param array $roles + * @return bool + */ + public function is($roles) + { + if(!is_array($roles)) + { + $roles = array($roles); + } + + // If the role has an aliased name then make sure we grab the correct name of + // the role from the aliases array. + $aliases = $this->feather['config']->get('feather: auth.aliases'); + + foreach($roles as $key => $role) + { + if(array_key_exists(strtolower($role), $aliases)) + { + $roles[$key] = $aliases[strtolower($role)]; + } + } + + foreach($this->user->roles as $role) + { + if(in_array(strtolower($role->name), $roles)) + { + return true; + } + + // Because some roles can be granted a super moderator permission, if we + // are checking that a user is a moderator we'll also check that the role + // is a super moderator. + if(in_array('moderator', $roles) and $role->super_moderator) + { + return true; + } + } + + return false; + } + + /** + * Determines if a user is not a given role or roles. + * + * @param array $roles + * @return bool + */ + public function not($roles) + { + return !$this->is($roles); + } + + /** + * Determines if a user is online. + * + * @return bool + */ + public function online() + { + return !$this->is('guest'); + } + + /** + * Determines if a user is offline. + * + * @return bool + */ + public function offline() + { + return !$this->online(); + } + + /** + * Determines if a user has activated their account. + * + * @return bool + */ + public function activated() + { + return (bool) $this->user->activated; + } + + /** + * Logs a user out and flushes the session. + * + * @return void + */ + public function logout() + { + Session::flush(); + + Auth::logout(); + } + + /** + * Route methods that are uncallable through to Laravel's Auth class. This provids a clean + * interface for accessing methods on that class. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return call_user_func_array(array('Auth', $method), $parameters); + } + +} \ No newline at end of file diff --git a/components/auth/driver.php b/components/auth/driver.php new file mode 100644 index 0000000..a63b064 --- /dev/null +++ b/components/auth/driver.php @@ -0,0 +1,121 @@ +find($id); + + // To provide a cleaner syntax when accessing roles, we'll make each of the roles keys + // the same as its ID. + $roles = array(); + + foreach($user->roles as $role) $roles[$role->id] = $role; + + $user->relationships['roles'] = $roles; + + return $user; + }, Core\User::cache_time); + } + else + { + return Cache::remember('guest', function() + { + return new Core\User(array( + 'roles' => array(3 => Core\Role::find(3)) + ), true); + }, Core\User::cache_time); + } + } + + /** + * Attempt to log a user in and if need be migrate them from an older system. + * + * @param array $credentials + * @return bool|object + */ + public function attempt($credentials = array()) + { + // Find the user in the database with the username they have provided in the + // login form. If no user can be found then that's as far as we go. + if(!$user = Core\User::where_username($credentials['username'])->first()) + { + return false; + } + + // If there is an active migration from another forum we'll need to perform + // a couple of extra checks. A migrating user has only a single role, which + // is the migrating roll. If the user has not been migrated then we'll + // attempt to migrate them. + if($migration = Core\Migration::active()) + { + $driver = Migrator\Drivers\Driver::make($migration); + + if($user->roles()->only('roles.id') == 6 and ($old = $driver->login($credentials))) + { + // Because the users password is most likely hashed - it damn + // well better be - we'll replace the password key in the array + // with the password they logged in with. That way Feather can + // re-hash it when storing it. + $old = (object) array_merge((array) $old, array('password' => $credentials['password'])); + + // Finally attempt to migrate the user. If the migration fails it's + // logged so that the administrator can take further action if + // required. + if(!$user = $driver->migrate_user($old, $user)) + { + dd('Failed to migrate user.'); + + return false; + } + } + else + { + return false; + } + } + else + { + // For a user that doesn't need to be migrated a simple hash check is in order. + if(!Hash::check($credentials['password'], $user->password)) + { + return false; + } + } + + return $this->login($user, array_get($credentials, 'remember')); + } + + /** + * Log a user in to Feather. + * + * @param int|Feather\Core\User $user + * @param bool $remember + * @return bool + */ + public function login($user, $remember = false) + { + if($user instanceof Core\User) + { + $user = $user->id; + } + + return parent::login($user, $remember); + } + +} \ No newline at end of file diff --git a/components/auth/migrator/drivers/driver.php b/components/auth/migrator/drivers/driver.php new file mode 100644 index 0000000..a3fea87 --- /dev/null +++ b/components/auth/migrator/drivers/driver.php @@ -0,0 +1,118 @@ +connection = $connection; + + $this->feather = $feather; + } + + /** + * Loads a migration driver. + * + * @param Feather\Components\Foundation\Application $feather + * @param Feather\Models\Migration $migration + * @return object + */ + public static function make(\Feather\Components\Foundation\Application $feather, \Feather\Models\Migration $migration) + { + $connection = DB::connection(FEATHER_DATABASE); + + if(!is_null($migration->database)) + { + $feather['config']->set('laravel: database.connections.feather:migration', array( + 'driver' => 'mysql', + 'host' => $migration->host, + 'database' => $migration->database, + 'username' => $migration->username, + 'password' => $migration->password, + 'charset' => 'utf8', + 'prefix' => '' + )); + + $connection = DB::connection('feather:migration'); + } + + if(isset(static::$extenders[$migration->driver])) + { + $resolver = static::$extenders[$migration->driver]; + + return static::$driver = $resolver($connection, $feather); + } + + switch($migration->driver) + { + case 'flux': + return static::$driver = new Flux($connection, $feather); + break; + } + } + + /** + * Register a custom migration driver. + * + * @param string $driver + * @param Closure $resolver + * @return void + */ + public static function extend($driver, $resolver) + { + static::$extenders[$driver] = $resolver; + } + + /** + * Return the database connection for method chaining. + * + * @return Laravel\Database\Connection + */ + public function db() + { + return $this->connection; + } + + /** + * Attempt to log a user in on the system. + * + * @param array $credentials + * @return bool|object + */ + abstract public function login($credentials); + +} \ No newline at end of file diff --git a/components/auth/migrator/drivers/flux.php b/components/auth/migrator/drivers/flux.php new file mode 100644 index 0000000..e54ba27 --- /dev/null +++ b/components/auth/migrator/drivers/flux.php @@ -0,0 +1,23 @@ +db()->where_username($credentials['username'])->first(); + + if(is_null($user) or sha1($credentials['password']) != $user->password) + { + return false; + } + + return $user; + } + +} \ No newline at end of file diff --git a/components/auth/sso.php b/components/auth/sso.php new file mode 100644 index 0000000..e3b81cf --- /dev/null +++ b/components/auth/sso.php @@ -0,0 +1,164 @@ + null, + 'username' => null, + 'email' => null, + 'token' => null + ); + + /** + * Authorizes a user from an external application. + * + * @param array $credentials + * @return mixed + */ + public function authorize(array $credentials) + { + $credentials = array_merge($this->defaults, $credentials); + + // If the current request is POST then the user is either registering or + // connecting an account. + if(Request::method() == 'POST') + { + $method = Input::has('create') ? 'register' : 'connect'; + + return $this->{$method}($credentials); + } + + $required = array('id', 'email', 'username'); + + foreach($required as $credential) + { + if(!array_key_exists($credential, $credentials) or is_null($credentials[$credential])) + { + return; + } + } + + extract($credentials); + + // If a user exists with an associated e-mail address that matches then this user has already + // been authenticated with a SSO service. + if($user = User::where_authenticator_associated_email($email)->first()) + { + // An authenticators token may change from time to time. If the tokens are no longer + // the same then the token must be updated. + if($user->authenticator_token != $token) + { + $user->authenticator_token = $token; + + $user->save(); + } + + $this->feather['auth']->login($user); + + return $this->feather['redirect']->to_self(); + } + + // If there is a user in the database who has the same e-mail or username as our + // user then they need to either link their account or create a new one. + elseif($user = User::where_username($username)->or_where('email', '=', $email)->first()) + { + $this->feather['crumbs']->drop(__('feather core::titles.connect_to_community')); + + return View::of('template')->with('title', __('feather core::titles.connect_to_community')) + ->nest('content', 'feather core::user.associate', compact('user')); + } + + // If there is no user in the database then the user does not need to perform any action to + // connect or create their account. We can simply authorize them to continue using Feather. + // ALl of this is behind the scenes and very quick, the user sees nothing. + $user = User::associate($credentials, $credentials); + + $this->feather['auth']->login($user); + + return $this->feather['redirect']->to_self(); + } + + /** + * Attempt to create a user to be associated with the foreign application. + * + * @param array $associate + * @return object + */ + protected function create($associate) + { + try + { + $this->feather['validator']->get('auth.sso.create')->against(Input::get())->passes(); + } + catch (FeatherValidationException $errors) + { + return $this->feather['redirect']->to_self()->with_input()->with_errors($errors->get()); + } + + Input::replace(array( + 'username' => Input::get('create_username'), + 'email' => Input::get('create_email') + )); + + if($user = User::associate($associate, $input)) + { + $this->feather['auth']->login($user); + + return $this->feather['redirect']->to_self(); + } + else + { + return $this->feather['redirect']->to_self()->with_input()->alert('failed', 'feather core::register.failure'); + } + } + + /** + * Attempts to connect a user to an existing Feather account. + * + * @param array $associate + * @return object + */ + protected function connect($associate) + { + try + { + $this->feather['validator']->get('auth.connect')->against(Input::get())->passes(); + } + catch (FeatherValidationException $errors) + { + return $this->feather['redirect']->to_self()->with_input()->with_errors($errors->get()); + } + + $user = User::where_email(Input::get('connect_email'))->first(); + + // If we are able to login with the username retrieved from the database and the password + // provided by the user then we can update the assoicated e-mail and redirect the user. + if($this->attempt(array('username' => $user->username, 'password' => Input::get('connect_password')))) + { + $user->fill(array( + 'authenticator' => $this->feather['config']->get('feather: db.auth.driver'), + 'authenticator_associated_email' => $associate['email'], + 'authenticator_token' => $associate['token'] + )); + + $user->save(); + + return $this->feather['redirect']->to_self(); + } + + return $this->feather['redirect']->to_self()->with_input()->alert('error', 'feather core::connect.failure'); + } + +} \ No newline at end of file diff --git a/components/config/repository.php b/components/config/repository.php new file mode 100644 index 0000000..263f01e --- /dev/null +++ b/components/config/repository.php @@ -0,0 +1,311 @@ +table('config')->get() as $item) + { + array_set($config, $item->key, str_replace('(:feather)', '(:bundle)', $item->value)); + } + + return $config; + }); + + foreach($items as $key => $item) + { + array_set(Config::$items['feather']['db'], $key, $item); + } + } + + /** + * Reload the database configuration items. + * + * @return void + */ + public function reload() + { + Cache::forget('config'); + + unset(Config::$items['feather']['db']); + + $this->db(); + } + + /** + * Determine if a key exists. + * + * @param string $key + * @return bool + */ + public function has($key) + { + return !is_null($this->get($key)); + } + + /** + * Set a config key to a given value. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function set($key, $value) + { + // If the key does not exist and it belongs to the database configuration the item is + // dirty and needs to be inserted. + if(!$this->has($key) and starts_with($this->idenfitier($key), 'db')) + { + $this->dirty[] = substr($this->idenfitier($key), 3); + } + + return Config::set($this->prefix($key), $value); + } + + /** + * Get a key from the configuration. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function get($key, $default = null) + { + return Config::get($this->prefix($key), $default); + } + + /** + * Save a key, group of keys, or all configuration items to the database. + * Only keys that belong to the database configuration can be saved. + * + * @param array $keys + * @return void + */ + public function save($keys = array()) + { + $items = array(); + + if($keys) + { + // Each key must belong to the database set of configuration items to be saved + // back into the database. + foreach((array) $keys as $key) + { + if(starts_with($this->idenfitier($key), 'db')) + { + $items[substr($this->idenfitier($key), 3)] = $this->get($key); + } + } + } + else + { + $items = $this->get('feather: db'); + } + + $update = $insert = array(); + + foreach($items as $key => $value) + { + // Using variable variables we can assign the item to the correct array + // depending on whether or not the item already exists within the + // database. + $variable = in_array($key, $this->dirty) ? 'insert' : 'update'; + + if(is_array($value)) + { + foreach($value as $key => $value) + { + ${$variable}[] = compact('key', 'value'); + } + + continue; + } + + ${$variable}[] = compact('key', 'value'); + } + + // Using PDO transactions we'll spin through our array of keys to update and execute + // the query for each of them. If something goes wrong the transaction will be rolled + // back automatically. + if($update) + { + DB::connection(FEATHER_DATABASE)->transaction(function() use ($update) + { + foreach($update as $item) + { + DB::connection(FEATHER_DATABASE)->table('config')->where_key($item['key'])->update(array('value' => $item['value'])); + } + }); + } + + // Because inserts behave differently to an update we can perform a batch insert with + // Fluent by providing an array of arrays. We'll use this method instead of transactions. + if($insert) + { + DB::connection(FEATHER_DATABASE)->table('config')->insert($insert); + } + } + + /** + * Delete a key or group of keys from the database. + * Only keys that belong to the database configuration can be deleted. + * + * @param array $keys + * @return void + */ + public function delete($keys) + { + $delete = array(); + + // Each key must belong to the database set of configuration items to be deleted + // from the database. + foreach((array) $keys as $key) + { + if(starts_with($this->idenfitier($key), 'db')) + { + $delete[] = substr($this->idenfitier($key), 3); + } + } + + if($delete) + { + DB::connection(FEATHER_DATABASE)->table('config')->where_in('key', $delete)->delete(); + } + } + + /** + * Determine the prefix for a given key. + * + * @param string $key + * @return string + */ + private function prefix($key) + { + if(!str_contains($key, ': ')) + { + return $key; + } + + list($prefix, $key) = explode(': ', $key); + + // Applications can refer to their own configuration files the same way that + // they are registered with Feather. + if(str_contains($prefix, ' ')) + { + list(, $app) = explode(' ', $prefix); + + $prefix = 'app'; + } + + switch($prefix) + { + // The feather prefix is applied to both database and file-based + // configuration items located within the bundle. + case 'feather': + return "feather::{$key}"; + break; + + // The gear prefix is applied to Feather gears, the gear + // name should appear before the file and key to be used. + case 'gear': + list($gear, $key) = explode(' ', $key); + + return "feather::gear:{$gear} {$key}"; + break; + + // The theme prefix is applied to Feather themes, the theme + // name should appear before the file and key to be used. + case 'theme': + list($theme, $key) = explode(' ', $key); + + return "feather::theme:{$theme} {$key}"; + break; + + case 'app': + return "feather {$app}::{$key}"; + break; + + default: + return $key; + } + } + + /** + * Determine the idenfitier excluding any namespacing. + * + * @param string $key + * @return string + */ + private function idenfitier($key) + { + if(!str_contains($key, '::') and !str_contains($key = $this->prefix($key), '::')) + { + return null; + } + + list($namespace, $idenfitier) = explode('::', $key); + + return $idenfitier; + } + +} \ No newline at end of file diff --git a/components/foundation/application.php b/components/foundation/application.php new file mode 100644 index 0000000..54e7e8a --- /dev/null +++ b/components/foundation/application.php @@ -0,0 +1,97 @@ +container[$key]); + } + + /** + * Get the value at a given offset. + * + * @param string $key + * @return mixed + */ + public function offsetGet($key) + { + if(!$this->offsetExists($key)) + { + throw new InvalidArgumentException("Identifier {$key} is not in the container."); + } + + return $this->container[$key]($this); + } + + /** + * Set the value at a given offset. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function offsetSet($key, $value) + { + if(!$value instanceof Closure) + { + $value = function() use ($value) + { + return $value; + }; + } + + $this->container[$key] = $value; + } + + /** + * Unset a given offset. + * + * @param string $key + * @return void + */ + public function offsetUnset($key) + { + unset($this->container[$key]); + } + +} \ No newline at end of file diff --git a/components/foundation/component.php b/components/foundation/component.php new file mode 100644 index 0000000..8429ec3 --- /dev/null +++ b/components/foundation/component.php @@ -0,0 +1,23 @@ +feather = $feather; + } + +} \ No newline at end of file diff --git a/components/gear/container.php b/components/gear/container.php new file mode 100644 index 0000000..abb3ce8 --- /dev/null +++ b/components/gear/container.php @@ -0,0 +1,73 @@ +container[$key]); + } + + /** + * Get the value at a given offset. + * + * @param string $key + * @return mixed + */ + public function offsetGet($key) + { + if(!$this->offsetExists($key)) + { + throw new InvalidArgumentException("Identifier {$key} is not in the container."); + } + + return $this->container[$key]($this); + } + + /** + * Set the value at a given offset. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function offsetSet($key, $value) + { + if(!$value instanceof Closure) + { + $value = function() use ($value) + { + return $value; + }; + } + + $this->container[$key] = $value; + } + + /** + * Unset a given offset. + * + * @param string $key + * @return void + */ + public function offsetUnset($key) + { + unset($this->container[$key]); + } + +} \ No newline at end of file diff --git a/components/gear/foundation.php b/components/gear/foundation.php new file mode 100644 index 0000000..5ce2ea2 --- /dev/null +++ b/components/gear/foundation.php @@ -0,0 +1,107 @@ +event('listen', $event, $handler); + } + + /** + * Overrides existing events listening for an event and fire + * a method or closure handler. + * + * @param string $event + * @param string|Closure $handler + * @return void + */ + public function override($event, $handler) + { + $this->event('override', $event, $handler); + } + + /** + * Binds different types of events. + * + * @param string $type + * @param string $event + * @param string|Closure $handler + * @return void + */ + private function event($type, $event, $handler) + { + $gear = $this; + + Event::$type($event, function($parameters = array()) use ($gear, $handler) + { + // If the handler is a callable closure then we'll execute the closure + // and return its result. + if(is_callable($handler)) + { + return $handler($parameters); + } + + // If the handler is a method that exists on the gear object then execute + // the method and return its result. + elseif(method_exists($gear, $handler)) + { + return call_user_func_array(array($gear, $handler), $parameters); + } + + // By default we'll return null, as the event manager will expect a non + // null result. + return null; + }); + + Event::fire('event'); + } + + /** + * Executed when a gear is enabled. + * + * @return void + */ + public function enabled(){} + + /** + * Executed when a gear is disabled. + * + * @return void + */ + public function disabled(){} + + /** + * Executed when a gear is removed. + * + * @return void + */ + public function remove(){} + + /** + * Set the Feather application instance. + * + * @param Feather\Components\Foundation\Application $feather + * @return void + */ + public function application(\Feather\Components\Foundation\Application $feather) + { + $this->feather = $feather; + } + +} \ No newline at end of file diff --git a/components/gear/manager.php b/components/gear/manager.php new file mode 100644 index 0000000..d3703f0 --- /dev/null +++ b/components/gear/manager.php @@ -0,0 +1,244 @@ +identifier, array( + 'location' => 'path: ' . path('gears') . $gear->identifier, + 'handles' => trim(str_replace('(:feather)', Bundle::option('feather', 'handles'), $gear->handles), '/'), + 'auto' => (bool) $gear->auto, + 'autoloads' => (array) json_decode($gear->loads) + )); + + if($gear->auto) + { + Bundle::start($gear->identifier); + } + + $manager->register($gear); + } + }); + } + + /** + * Register a Gear with the manager. + * + * @param Feather\Models\Gear $gear + * @return bool + */ + public function register(Gear $gear) + { + if(!$gear instanceof Gear) + { + throw new InvalidArgumentException('Supplied Gear is a not supported type.'); + } + + if(starts_with($gear->location, 'path: ')) + { + $path = substr($gear->location, 6); + } + else + { + $path = path('gears') . $gear->location; + } + + if(file_exists($path)) + { + $this->gears[$gear->identifier] = $gear; + + // Start any gears that have been set to automatically start. + if($gear->auto) + { + return $this->start($gear->identifier); + } + } + + return true; + } + + /** + * Determines if a Gear has been registered. + * + * @param string $gear + * @return bool + */ + public function registered($gear) + { + return isset($this->gears[$gear]); + } + + /** + * Start a registered Gear. + * + * @param string $gear + * @return bool + */ + public function start($gear) + { + if($this->started($gear)) + { + return $this->started[$gear]; + } + elseif(!$this->registered($gear)) + { + return false; + } + + $this->started[$gear] = new Container; + + // Spin through all the files within the gears directory and those that have + // the .gear.php suffix we'll require and hopefully be able to instantiate + // a new object. + if(starts_with($this->gears[$gear]->location, 'path: ')) + { + $path = substr($this->gears[$gear]->location, 6); + } + else + { + $path = path('gears') . $this->gears[$gear]->location; + } + + foreach(new FilesystemIterator($path) as $file) + { + if(ends_with($file->getFilename(), '.gear.php')) + { + require_once $file->getPathname(); + + $name = str_replace('.gear.php', null, $file->getFilename()); + + $class = "\\Feather\\Gear\\{$this->gears[$gear]->identifier}\\{$name}"; + + if(class_exists($class)) + { + $this->started[$gear][$name] = new $class; + + $this->started[$gear][$name]->application($this->feather); + } + } + } + + return $this->started[$gear]; + } + + /** + * Determines if a Gear has been started. + * + * @param string $gear + * @return bool + */ + public function started($gear) + { + return isset($this->started[$gear]); + } + + /** + * Disable a gear for this run only. + * + * @param string $gear + * @return void + */ + public function disable($gear) + { + if($this->registered($gear)) + { + unset($this->gears[$gear]); + } + } + + /** + * Gets a Gears container. + * + * @param string $gear + * @return object + */ + public function get($gear) + { + if(!$this->started($gear) or !$this->registered($gear)) + { + return false; + } + + return $this->started[$gear]; + } + + /** + * Fire an event and implodes the returned results. + * + * @param string $event + * @param array $parameters + * @return string + */ + public function fire($event, $parameters = array()) + { + return implode(PHP_EOL, array_filter(Event::fire($event, array($parameters)))); + } + + /** + * Fire the first event in the queue. + * + * @param string $event + * @param array $parameters + * @return string + */ + public function first($event, $parameters = array()) + { + return Event::first($event, $parameters); + } + + /** + * Fire the first controller event in the queue. + * + * @param object $controller + * @return mixed + */ + public function controller($event, $controller) + { + $route = Request::route(); + + $method = Str::lower($route->method); + + return Event::first("controller: {$event} {$route->controller}@{$method}.{$route->controller_action}", array(array($controller))); + } + +} \ No newline at end of file diff --git a/components/pagination/paginator.php b/components/pagination/paginator.php new file mode 100644 index 0000000..7bffc5a --- /dev/null +++ b/components/pagination/paginator.php @@ -0,0 +1,92 @@ +page = $page; + $this->last = $last; + $this->total = $total; + $this->results = $results; + $this->per_page = $per_page; + } + + /** + * Create a new Paginator instance. + * + * @param array $results + * @param int $total + * @param int $per_page + * @return Paginator + */ + public static function make($results, $total, $per_page) + { + $page = static::page($total, $per_page); + + $last = ceil($total / $per_page); + + return new static($results, $page, $total, $per_page, $last); + } + + /** + * Get the current page from the last segment of the URI. + * + * @param int $total + * @param int $per_page + * @return int + */ + public static function page($total, $per_page) + { + if(!is_numeric($page = substr(URI::$segments[count(URI::$segments) - 1], 1))) + { + $page = 1; + } + + // The page will be validated and adjusted if it is less than one or greater + // than the last page. For example, if the current page is not an integer or + // less than one, one will be returned. If the current page is greater than + // the last page, the last page will be returned. + if (is_numeric($page) and $page > ($last = ceil($total / $per_page))) + { + return ($last > 0) ? $last : 1; + } + + return $page; + } + + /** + * Create a HTML page link. + * + * @param int $page + * @param string $text + * @param string $class + * @return string + */ + protected function link($page, $text, $class) + { + $page = "p{$page}"; + + if(preg_match('/p([0-9]+)/', $page)) + { + $uri = preg_replace('/\/p([0-9]+)/', '', URI::current(), 1); + } + + return HTML::link($uri . '/' . $page, $text, compact('class'), Request::secure()); + } + +} \ No newline at end of file diff --git a/components/support/breadcrumbs.php b/components/support/breadcrumbs.php new file mode 100644 index 0000000..b9b6c9e --- /dev/null +++ b/components/support/breadcrumbs.php @@ -0,0 +1,109 @@ +place($crumb); + } + else + { + // If the crumb is an object, such as a Laravel Messages object, then convert it + // to a string as any objects being passed to this method will have a __toString() + if(is_object($crumb)) + { + $crumb = array((string) $crumb); + } + elseif(!is_array($crumb)) + { + $crumb = array($crumb); + } + + // Fetch the title of the crumb and the crumbs link. If the crumb does not have + // a supplied link then use the current page. + $title = isset($crumb['title']) ? $crumb['title'] : array_shift($crumb); + + $link = isset($crumb['link']) ? $crumb['link'] : URL::current(); + + $this->crumbs[] = (object) compact('link', 'title'); + } + + return $this; + } + + /** + * Leaves a trail of crumbs. + * + * @return string + */ + public function trail($element = 'li') + { + $response = array( + $this->item(URL::to_route('feather'), $this->feather['config']->get('feather: db.forum.title'), $element) + ); + + foreach($this->crumbs as $crumb) + { + $response[] = $this->item($crumb->link, $crumb->title, $element); + } + + return implode(PHP_EOL, $response); + } + + /** + * Adds a place to the trail. + * + * @param Feather\Core\Place $place + * @return array + */ + protected function place($place) + { + // Add each of the ancestors of this place to the crumbs. + foreach($place->crumbs() as $crumb) + { + $this->crumbs[] = (object) array( + 'link' => URL::to_route('place', array($crumb->id, $crumb->slug)), + 'title' => $crumb->name + ); + } + + return $this->crumbs[] = (object) array( + 'link' => URL::to_route('place', array($place->id, $place->slug)), + 'title' => $place->name + ); + } + + /** + * Return a list item with the link. + * + * @param string $link + * @param string $text + * @param string $element + * @return string + */ + public function item($link, $text, $element = 'li') + { + return "<{$element}>" . HTML::link($link, $text) . ""; + } + +} \ No newline at end of file diff --git a/components/support/date.php b/components/support/date.php new file mode 100644 index 0000000..e50fbdd --- /dev/null +++ b/components/support/date.php @@ -0,0 +1,205 @@ + array('second', 'minute', 'hour', 'day', 'week', 'month', 'year'), + 'lengths' => array(60, 60, 24, 7, 4.35, 12) + ); + + /** + * Alternate text to show if date is invalid. + * + * @var string + */ + public $alternate; + + /** + * Text to show before the returned formatted string. + * + * @var string + */ + public $prefix; + + /** + * Text to show after the returned formatted string. + * + * @var string + */ + public $suffix; + + /** + * The DateTime being used for the current instance. + * + * @var object + */ + public $date; + + /** + * Set the date to use. + * + * @param int|string $date + * @return Feather\Components\Support\Date + */ + public function set($date) + { + $this->date = new DateTime; + + if(is_numeric($date)) + { + $this->date->setTimestamp($date); + } + else + { + $this->date->setTimestamp(strtotime($date)); + } + + return $this; + } + + /** + * Format a date and return it for the discussion meta data. + * + * @param string|int $date + * @return string + */ + public function meta($date) + { + $this->set($date); + + return '' . $this->relative('short') . ''; + } + + /** + * Sets the alternate text to display if the date is of an invalid format. + * + * @param string $text + * @return Feather\Components\Support\Date + */ + public function alternate($text) + { + $this->alternate = $text; + + return $this; + } + + /** + * Sets the prefix text. + * + * @param string $text + * @return Feather\Components\Support\Date + */ + public function prefix($text) + { + $this->prefix = $text; + + return $this; + } + + /** + * Sets the suffix text. + * + * @param string $text + * @return Feather\Components\Support\Date + */ + public function suffix($text) + { + $this->suffix = $text; + + return $this; + } + + /** + * Returns the formatted date. + * + * @param string $format + * @return string + */ + public function show($format) + { + $formats = array( + 'long' => $this->feather['config']->get('feather: db.datetime.long_date'), + 'short' => $this->feather['config']->get('feather: db.datetime.short_date'), + 'time' => $this->feather['config']->get('feather: db.datetime.time_only') + ); + + if(isset($formats[$format])) + { + $format = $formats[$format]; + } + + $errors = $this->date->getLastErrors(); + + if($this->date->getTimestamp() == 0 or $errors['warning_count'] > 0 or $errors['error_count'] > 0) + { + return $this->alternate ?: 'Invalid date supplied.'; + } + + return $this->prefix . $this->date->format($format) . $this->suffix; + } + + /** + * Returns a fuzzy formated date. + * + * @return string + */ + public function fuzzy($stop = 'week', $format = 'long') + { + $difference = time() - $this->date->getTimestamp(); + + $offset = array_search($stop, $this->fuzzy['periods']) + 1; + + $periods = array_slice($this->fuzzy['periods'], 0, $offset); + + $lengths = array_slice($this->fuzzy['lengths'], 0, $offset); + + for($i = 0; $difference >= $lengths[$i] and $i < count($lengths) - 1; $i++) + { + $difference = $difference / $lengths[$i]; + } + + $difference = round($difference); + + if($difference != 1) + { + $periods[$i] .= 's'; + } + + if($difference > $lengths[$i]) + { + return $this->show($format); + } + + return $this->prefix . number_format($difference) . ' ' . $periods[$i] . ' ago' . $this->suffix; + } + + /** + * Returns a relative formated date. + * + * @return string + */ + public function relative($format = 'long') + { + $days = intval((time() - $this->date->getTimestamp()) / 86400); + + if($days == 0) + { + return __('feather core::common.today')->get() . ', ' . $this->show('time'); + } + elseif($days == 1) + { + return __('feather core::common.yesterday')->get() . ', ' . $this->show('time'); + } + + return $this->show($format); + } + +} \ No newline at end of file diff --git a/components/support/facade.php b/components/support/facade.php new file mode 100644 index 0000000..3b982e9 --- /dev/null +++ b/components/support/facade.php @@ -0,0 +1,56 @@ +with('alert', array('type' => $type, 'message' => __($key, $replacements))); + } + + /** + * Create a redirect response with the query string appended. + * + * @param string $url + * @return object + */ + public function with_query($url) + { + $url = URL::to($this->pattern($url)); + + if($query = urldecode(Request::getQueryString())) + { + $url = "{$url}?{$query}"; + } + + return $this->to($url); + } + + /** + * Create a redirect response to the current page. + * + * @return object + */ + public function to_self() + { + return $this->with_query(URI::current()); + } + + /** + * Create a redirect response to the previous page. + * + * @param string $default + * @return object + */ + public function to_previous($default = null) + { + return $this->to(Input::has('return') ? Input::get('return') : $this->pattern($default)); + } + + /** + * Create a redirect response after a logout. + * + * @return object + */ + public function after_logout() + { + return $this->for_auth(Config::get('feather: db.auth.logout_url')); + } + + /** + * Create a redirect response before a registration. + * + * @return object + */ + public function before_register() + { + return $this->for_auth(Config::get('feather: db.auth.register_url')); + } + + /** + * Create a redirect response before a login. + * + * @return object + */ + public function before_login() + { + return $this->for_auth(Config::get('feather: db.auth.login_url')); + } + + /** + * Create a redirect response to an authentication page. + * + * @param string $url + * @return object + */ + protected function for_auth($url) + { + $replace = array( + '{feather}' => Bundle::option('feather', 'handles'), + '{current}' => URL::to(Input::has('return') ? Input::get('return') : URI::current()), + '{token}' => Auth::online() ? Auth::user()->authenticator_token : null + ); + + if(Config::get('feather: db.auth.driver') == 'internal') + { + $url = URL::to_route('feather'); + } + else + { + $url = str_replace(array_keys($replace), array_values($replace), $url); + } + + return $this->to($url); + } + + /** + * Builds a URL based on defined patterns. + * + * @param string $url + * @return string + */ + protected function pattern($url) + { + if(starts_with($url, 'route: ')) + { + return URL::to_route(substr($url, 7)); + } + elseif(starts_with($url, 'action: ')) + { + return URL::to_action(substr($url, 8)); + } + + return $url; + } + + /** + * Flash a Validator's errors to the session data. + * + * @param object|string $container + * @return object + */ + public function with_errors($container) + { + $errors = !($container instanceof Messages) ? new Messages(array($container)) : $container; + + return $this->with('errors', $errors); + } + +} \ No newline at end of file diff --git a/components/validation/validator.php b/components/validation/validator.php new file mode 100644 index 0000000..b7fe394 --- /dev/null +++ b/components/validation/validator.php @@ -0,0 +1,194 @@ +application = $application; + + return $this; + } + + /** + * Get a validator. + * + * @param string $validator + * @return Feather\Components\Support\Validation + */ + public function get($validator) + { + $segments = explode('.', $validator); + + if(file_exists($path = path('applications') . $this->application . DS . 'validation' . DS . $segments[0] . EXT)) + { + if($this->validating[$validator] = array_get(require $path, implode('.', array_slice($segments, 1)) ?: null)) + { + return $this; + } + } + + if($this->validating[$validator] = $this->feather['config']->get("feather {$this->application}: validation.{$validator}")) + { + return $this; + } + + throw new InvalidArgumentException("Invalid validator [{$validator}] supplied."); + } + + /** + * Sets input to validate against. + * + * @param array $input + * @return Feather\Components\Support\Validation + */ + public function against($input) + { + $this->input = $input; + + return $this; + } + + /** + * Executes validation and returns true on success. + * + * @return bool + */ + public function passes() + { + // If a response is returned from the closure then we have a custom error messages + // to throw with the exception. + if($responses = call_user_func_array(reset($this->validating), array($this, $this->feather))) + { + if(!is_array(reset($responses))) $responses = array(array_shift($responses) => array()); + + foreach($responses as $response => $replacements) + { + $responses[$response] = __("feather {$this->application}::{$response}", $replacements)->get(); + } + + throw new FeatherValidationException(new Messages($responses)); + } + + $event = key($this->validating); + + $this->feather['gear']->fire("validation: before {$event}", array($this)); + + $validator = new \Validator($this->input, $this->rules, $this->messages); + + if($validator->connection(DB::connection(FEATHER_DATABASE))->fails()) + { + throw new FeatherValidationException($validator->errors); + } + + return true; + } + + /** + * Adds a rule or array of rules to the rules array. + * + * @param string $name + * @param array|string $rule + * @return Feather\Components\Support\Validation + */ + public function rule($name, $rule) + { + if(!isset($this->rules[$name])) + { + $this->rules[$name] = array(); + } + + $this->rules[$name] = array_merge($this->rules[$name], (array) $rule); + + return $this; + } + + /** + * Adds a message to the messages array. + * + * @param string $rule + * @param array|string $message + * @return Feather\Components\Support\Validation + */ + public function message($rule, $message) + { + $replacements = array(); + + if(is_array($message)) + { + list($message, $replacements) = $message; + } + + $this->messages[str_replace('.', '_', $rule)] = __("feather {$this->application}::{$message}", $replacements)->get(); + + return $this; + } + + /** + * Adds a data key/value pair to the data array. + * + * @param string $key + * @param mixed $value + * @return Feather\Components\Support\Validation + */ + public function with($key, $value) + { + $this->data[$key] = $value; + + return $this; + } + +} \ No newline at end of file diff --git a/composer.json b/composer.json deleted file mode 100644 index 37cafb4..0000000 --- a/composer.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "require": { - "illuminate/foundation": ">=1.0.0", - "jasonlewis/basset": "*" - }, - "require-dev": { - "mockery/mockery": "0.7.*" - }, - "autoload": { - "classmap": [ - "feather/Auth", - "feather/Console", - "feather/Database", - "feather/Extensions", - "feather/Migrator", - "feather/Presenter", - "app/controllers", - "app/models", - "app/extensions", - "app/database/migrations", - "tests/TestCase.php" - ] - }, - "minimum-stability": "dev" -} \ No newline at end of file diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 0000000..186a23b --- /dev/null +++ b/config/auth.php @@ -0,0 +1,174 @@ + array( + 'admin' => 'administrator' + ), + + /* + |-------------------------------------------------------------------------- + | Action Rules + |-------------------------------------------------------------------------- + | + | Auth uses action rules when checking a user has permission to perform + | a given action. + | + | For example, a user may delete users but may not delete themselves. To + | do this we create a rule for that verb and action. + | + | Rules a placed within the verb for an action. The verbs are: 'access', + | 'use', 'start', 'edit', 'delete', 'approve', and 'moderate'. + | + */ + + 'rules' => array( + + /* + |-------------------------------------------------------------------------- + | Delete Verb + |-------------------------------------------------------------------------- + */ + + 'delete' => array( + + /* + |-------------------------------------------------------------------------- + | Deleting Users + |-------------------------------------------------------------------------- + | + | Users that can delete other users are not able to delete themselves. + | + */ + + 'users' => function($deleting) + { + return ($deleting->id != Feather\Auth::user()->id); + } + ), + + /* + |-------------------------------------------------------------------------- + | Edit Verb + |-------------------------------------------------------------------------- + */ + + 'edit' => array( + + /* + |-------------------------------------------------------------------------- + | Editing Discussion + |-------------------------------------------------------------------------- + | + | Users can edit their own discussions, unless they are Administrator or + | Moderator, they can edit all. + | + */ + + 'discussion' => function($discussion) + { + $user = Feather\Auth::user(); + + if(Feather\Auth::is('admin')) + { + return true; + } + elseif(Feather\Auth::is('moderator')) + { + return true; + } + + // The easy checks have been done, now to see if the user is a place specific + // moderator on the discussions place. + if($discussion->place->moderators) + { + foreach($discussion->place->moderators as $moderator) + { + if($user->id == $moderator->user_id) + { + return true; + } + } + } + + // Lastly if this discussion belongs to this user and they have permission to + // edit their own discussions we'll allow them to edit this discussion. + if($user->id == $discussion->user_id and Feather\Auth::can('edit: own discussions')) + { + return true; + } + + return false; + } + + ), + + /* + |-------------------------------------------------------------------------- + | View Verb + |-------------------------------------------------------------------------- + */ + + 'view' => array( + + /* + |-------------------------------------------------------------------------- + | Viewing Discussion + |-------------------------------------------------------------------------- + | + | Users can view all discussions unless it is either private or a draft. + | + */ + + 'discussion' => function($discussion) + { + $user = Feather\Auth::user(); + + // Private discussions can only be viewed by participants, the original + // poster, and administrators/moderators. + if($discussion->private) + { + if($discussion->user_id == $user->id or Feather\Auth::is(array('admin', 'moderator'))) + { + return true; + } + + foreach($discussion->participants as $participant) + { + if(Feather\Auth::online() and $participant->user_id == $user->id) + { + return true; + } + } + + return false; + } + + // Draft discussions can only be viewed by the original author and + // administrators/moderators. + elseif($discussion->draft) + { + if($discussion->user_id == $user->id or Feather\Auth::is(array('admin', 'moderator'))) + { + return true; + } + + return false; + } + + return true; + } + + ) + ) + +); \ No newline at end of file diff --git a/config/feather.php b/config/feather.php new file mode 100644 index 0000000..ff02cbf --- /dev/null +++ b/config/feather.php @@ -0,0 +1,117 @@ + array( + 'host' => 'localhost', + 'database' => 'feather', + 'username' => 'root', + 'password' => '', + 'prefix' => '', + 'charset' => 'utf8', + 'driver' => 'mysql' + ), + + /* + |-------------------------------------------------------------------------- + | Feather Applications + |-------------------------------------------------------------------------- + | + | Applications to be registered at runtime with Feather. It is advised you + | do not edit anything down there. + | + */ + + 'applications' => array( + 'admin' => '(:feather)/admin', + 'core' => '(:feather)' + ), + + /* + |-------------------------------------------------------------------------- + | Feather Components + |-------------------------------------------------------------------------- + | + | Components to be registered at runtime with Feather. It is advised you + | do not edit anything down there. + | + */ + + 'components' => array( + 'auth' => function($feather) + { + $feather['auth'] = $feather->share(function($feather) + { + return new Feather\Components\Auth\Authorizer($feather); + }); + }, + 'sso' => function($feather) + { + $feather['sso'] = $feather->share(function($feather) + { + return new Feather\Components\Auth\SSO($feather); + }); + }, + 'gear' => function($feather) + { + $feather['gear'] = $feather->share(function($feather) + { + return new Feather\Components\Gear\Manager($feather); + }); + }, + 'redirect' => function($feather) + { + $feather['redirect'] = function($feather) + { + return new Feather\Components\Support\Redirector(null); + }; + }, + 'breadcrumbs' => function($feather) + { + $feather['breadcrumbs'] = $feather->share(function($feather) + { + return new Feather\Components\Support\Breadcrumbs($feather); + }); + }, + 'validator' => function($feather) + { + $feather['validator'] = function($feather) + { + return new Feather\Components\Validation\Validator($feather); + }; + }, + 'date' => function($feather) + { + $feather['date'] = function($feather) + { + return new Feather\Components\Support\Date($feather); + }; + }, + 'paginator' => function($feather) + { + $feather['paginator'] = function($feather) + { + return new Feather\Components\Pagination\Paginator($feather); + }; + } + ), +); \ No newline at end of file diff --git a/facades.php b/facades.php new file mode 100644 index 0000000..16e4b1f --- /dev/null +++ b/facades.php @@ -0,0 +1,100 @@ +app['auth']->extend('feather', function($app) - { - $provider = new FeatherUserProvider($app['hash'], $app['feather']['migrator']); - - return new Guard($provider, $app['session']); - }); - - $this->app['config']->set('auth.driver', 'feather'); - } - -} \ No newline at end of file diff --git a/feather/Auth/FeatherUserProvider.php b/feather/Auth/FeatherUserProvider.php deleted file mode 100644 index 3a7d79a..0000000 --- a/feather/Auth/FeatherUserProvider.php +++ /dev/null @@ -1,97 +0,0 @@ -hasher = $hasher; - $this->migrator = $migrator; - } - - /** - * Retrieve a user by their unique idenetifier. - * - * @param mixed $identifier - * @return Illuminate\Auth\UserInterface|null - */ - public function retrieveByID($identifier) - { - $user = User::find($identifier); - - if ( ! is_null($user)) - { - return $user; - } - } - - /** - * Retrieve a user by the given credentials. - * - * @param array $credentials - * @return Illuminate\Auth\UserInterface|null - */ - public function retrieveByCredentials(array $credentials) - { - // First we'll attempt to locate a user by checking for the username they provided when logging in. - if ( ! $user = User::where('username', $credentials['username'])->first()) - { - return; - } - - // If we have a migrator interface then we need confirm that the user has been migrated previously. If - // the user has not been migrated then we'll leave it to the migrator to handle it. - if ($this->migrator->isActive()) - { - dd('here'); - } - - - if ( ! is_null($user)) - { - return $user; - } - } - - /** - * Validate a user against the given credentials. - * - * @param Illuminate\Auth\UserInterface $user - * @param array $credentials - * @return bool - */ - public function validateCredentials(UserInterface $user, array $credentials) - { - $plain = $credentials['password']; - - return $this->hasher->check($plain, $user->getAuthPassword()); - } - -} \ No newline at end of file diff --git a/feather/Console/FeatherCommand.php b/feather/Console/FeatherCommand.php deleted file mode 100644 index dd85508..0000000 --- a/feather/Console/FeatherCommand.php +++ /dev/null @@ -1,30 +0,0 @@ -line('Feather version '.FEATHER_VERSION.''); - } -} \ No newline at end of file diff --git a/feather/Console/InstallCommand.php b/feather/Console/InstallCommand.php deleted file mode 100644 index 7cf0dab..0000000 --- a/feather/Console/InstallCommand.php +++ /dev/null @@ -1,30 +0,0 @@ -line('Feather version '.FEATHER_VERSION.''); - } -} \ No newline at end of file diff --git a/feather/Console/PublishCommand.php b/feather/Console/PublishCommand.php deleted file mode 100644 index 9dfe8a5..0000000 --- a/feather/Console/PublishCommand.php +++ /dev/null @@ -1,138 +0,0 @@ -publisher = $publisher; - $this->themePath = $themePath; - $this->extensionPath = $extensionPath; - } - - /** - * Execute the console command. - * - * @return void - */ - public function fire() - { - // The publish command encompasses both themes and extensions. Because of this a flag must be provided so that the - // publisher knows what's being published and where it is located. - if ( ! $this->input->getOption('theme') and ! $this->input->getOption('extension')) - { - $this->comment("Please use either the --theme or --extension flag."); - - return; - } - - $name = $this->input->getArgument('name'); - - // Using the AssetPublisher we can publish our themes or extensions to the correct destination within our public - // directory. If publishing fails we'll catch the exception and let the user no that they probably spelt the - // theme or extension incorrectly. - - if ($this->input->getOption('theme')) - { - try - { - $this->publisher->publish("{$this->themePath}/{$name}/public", "../feather/themes/{$name}"); - - $this->line("Successfully published theme: {$name}"); - } - catch (RuntimeException $error) - { - $this->comment('Failed to publish theme. There may be nothing to publish.'); - } - } - elseif ($this->input->getOption('extension')) - { - try - { - $this->publisher->publish("{$this->extensionPath}/{$name}/public", "../feather/extensions/{$name}"); - - $this->line("Successfully published extension: {$name}"); - } - catch (RuntimeException $error) - { - $this->comment('Failed to publish extension. There may be nothing to publish.'); - } - } - } - - /** - * Get the command arguments. - * - * @return array - */ - public function getArguments() - { - return array( - array('name', InputArgument::REQUIRED, 'Name of the theme or extension to publish') - ); - } - - /** - * Get the command options. - * - * @return array - */ - public function getOptions() - { - return array( - array('theme', null, InputOption::VALUE_NONE, 'Tells publisher to publish a theme'), - array('extension', null, InputOption::VALUE_NONE, 'Tells publisher to publish an extension'), - ); - } - -} \ No newline at end of file diff --git a/feather/Database/EloquentBuilder.php b/feather/Database/EloquentBuilder.php deleted file mode 100644 index a830db6..0000000 --- a/feather/Database/EloquentBuilder.php +++ /dev/null @@ -1,34 +0,0 @@ -model->cachable[$this->model->table])) - { - list($group, $foreign) = $this->model->cachable[$this->model->table]; - - if (Cache::has("{$group}_{$id}")) - { - return Cache::get("{$group}_{$id}"); - } - } - - $this->query->where($this->model->getKeyName(), '=', $id); - - return $this->first($columns); - } - -} \ No newline at end of file diff --git a/feather/Extensions/Dispatcher.php b/feather/Extensions/Dispatcher.php deleted file mode 100644 index 5d9af30..0000000 --- a/feather/Extensions/Dispatcher.php +++ /dev/null @@ -1,263 +0,0 @@ -files = $files; - $this->path = $path; - } - - /** - * Set the Laravel application instance. - * - * @param Illuminate\Foundation\Application $app - * @return void - */ - public function setApplication($app) - { - $this->app = $app; - } - - /** - * Register an array of extensions with the dispatcher. - * - * @param array $extensions - * @return void - */ - public function registerExtensions($extensions) - { - foreach ($extensions as $extension) - { - $this->register($extension); - } - } - - /** - * Get the started extensions. - * - * @return array - */ - public function getStarted() - { - return $this->started; - } - - /** - * Register an extension with the dispatcher. - * - * @param array $extension - * @return array - */ - public function register(array $extension) - { - $identifier = $extension['identifier']; - - $path = $this->path.'/'.$extension['location']; - - if ($this->files->exists($path)) - { - $extension['path'] = $path; - - $extension['loaded'] = array(); - - $this["extension.{$extension['identifier']}"] = $extension; - - // If an extension is set to be automatically started then we'll hand it off to - // the starting method. - if ($extension['auto']) - { - $this->start($extension['identifier']); - } - - return $this["extension.{$extension['identifier']}"]; - } - } - - /** - * Determine if an extension is regisetered. - * - * @param string $identifier - * @return bool - */ - public function isRegistered($identifier) - { - return isset($this["extension.{$identifier}"]); - } - - /** - * Determine if an extension is started. - * - * @param string $identifier - * @return bool - */ - public function isStarted($identifier) - { - return in_array($identifier, $this->started); - } - - /** - * Start an extension. - * - * @param string $identifier - * @return void - */ - public function start($identifier) - { - if ($this->isStarted($identifier) or ! $this->isRegistered($identifier)) - { - return; - } - - $extension = $this["extension.{$identifier}"]; - - foreach ($this->findExtensions($extension['path']) as $file) - { - $name = $file->getBasename(".{$file->getExtension()}"); - - if (ends_with($name, 'Extension')) - { - $location = str_replace('/', '\\', $extension['location']); - - $class = "Feather\\Extensions\\{$location}\\{$name}"; - - // Instantiate the new extension class and assign it to the extensions loaded classes. The class - // receives an instance of the Laravel application. - $extension['loaded'][$class] = $this->loadExtension($class); - - $extension['loaded'][$class]->start($this->app); - } - } - - $this["extension.{$identifier}"] = $extension; - - // Add the extension to the array of started extensions. - $this->started[] = $identifier; - } - - /** - * Get a FilesystemIterator to find the extensions within a path. - * - * @param string $path - * @return FilesystemIterator - */ - public function findExtensions($path) - { - return new FilesystemIterator($path); - } - - /** - * Create a new extension instance. - * - * @param string $class - * @return Feather\Extensions\Extension - */ - public function loadExtension($class) - { - return new $class($this->app); - } - - public function fire() - { - - } - - /** - * Determine if a given offset exists. - * - * @param string $key - * @return bool - */ - public function offsetExists($key) - { - return isset($this->extensions[$key]); - } - - /** - * Get the value at a given offset. - * - * @param string $key - * @return mixed - */ - public function offsetGet($key) - { - if ( ! isset($this->extensions[$key])) - { - throw new InvalidArgumentException("Type {$key} is not bound."); - } - - return $this->extensions[$key]; - } - - /** - * Set the value at a given offset. - * - * @param string $key - * @param mixed $value - * @return void - */ - public function offsetSet($key, $value) - { - $this->extensions[$key] = $value; - } - - /** - * Unset the value at a given offset. - * - * @param string $key - * @return void - */ - public function offsetUnset($key) - { - unset($this->extensions[$key]); - } - -} \ No newline at end of file diff --git a/feather/Extensions/Extension.php b/feather/Extensions/Extension.php deleted file mode 100644 index 010eec8..0000000 --- a/feather/Extensions/Extension.php +++ /dev/null @@ -1,77 +0,0 @@ -app = $app; - } - - /** - * Listen for an event and fire a method or closure handler. - * - * @param string $event - * @param string|Closure $handler - * @return void - */ - public function listen($event, $handler) - { - $this->bindEvent('listen', $event, $handler); - } - - /** - * Overrides existing events listening for an event and fire - * a method or closure handler. - * - * @param string $event - * @param string|Closure $handler - * @return void - */ - public function override($event, $handler) - { - $this->bindEvent('override', $event, $handler); - } - - /** - * Binds events for the extension. - * - * @param string $type - * @param string $event - * @param string|Closure $handler - * @return void - */ - private function bindEvent($type, $event, $handler) - { - // Set the current extension instance so we can use it within the event closure. - $extension = $this; - - $this->app['events']->$type($event, function($parameters = array()) use ($extension, $handler) - { - // If the handler is a callable closure then we'll execute the closure - // and return its result. - if (is_callable($handler)) - { - return $handler($parameters); - } - - // Finally assume that the handler is a method on the extension itself. - return call_user_func_array(array($extension, $handler), $parameters); - }); - } - -} \ No newline at end of file diff --git a/feather/Extensions/ExtensionInterface.php b/feather/Extensions/ExtensionInterface.php deleted file mode 100644 index b5041d6..0000000 --- a/feather/Extensions/ExtensionInterface.php +++ /dev/null @@ -1,41 +0,0 @@ -app['feather.extensions'] = $this->app->share(function($app) - { - return new Dispatcher($app['files'], $app['path.extensions']); - }); - - $this->app['feather.extensions']->setApplication($this->app); - } - -} \ No newline at end of file diff --git a/feather/Migrator/DriverInterface.php b/feather/Migrator/DriverInterface.php deleted file mode 100644 index 9d014ff..0000000 --- a/feather/Migrator/DriverInterface.php +++ /dev/null @@ -1,33 +0,0 @@ -app = $app; - } - - /** - * Set the migrator database record. - * - * @param Feather\Models\Migrator $migrator - */ - public function setMigratorRecord(Migrator $migrator) - { - $this->migratorRecord = $migrator; - - return $this; - } - - /** - * Determine if the driver has a migrator database record. - * - * @return bool - */ - public function hasMigratorRecord() - { - return ! is_null($this->migratorRecord); - } - -} \ No newline at end of file diff --git a/feather/Migrator/Drivers/FluxDriver.php b/feather/Migrator/Drivers/FluxDriver.php deleted file mode 100644 index abf1211..0000000 --- a/feather/Migrator/Drivers/FluxDriver.php +++ /dev/null @@ -1,35 +0,0 @@ -app = $app; - $this->connection = $connection; - $this->active = $active; - } - - /** - * Get the default migrator driver name. - * - * @return string - */ - protected function getDefaultDriver() - { - return $this->app['config']->get('feather::migrator.driver'); - } - - /** - * Call a custom driver creator. - * - * @param string $driver - * @return mixed - */ - protected function callCustomCreator($driver) - { - return $this->customCreators[$driver]($this->app, $this->connection); - } - - /** - * Create a new flux driver instance. - * - * @return Feather\Migrator\Drivers\FluxDriver - */ - protected function createFluxDriver() - { - return new Drivers\FluxDriver($this->app); - } - - /** - * Determine if the migrator is active. - * - * @return bool - */ - public function isActive() - { - return $this->active; - } - -} \ No newline at end of file diff --git a/feather/Migrator/MigratorServiceProvider.php b/feather/Migrator/MigratorServiceProvider.php deleted file mode 100644 index c940ce8..0000000 --- a/feather/Migrator/MigratorServiceProvider.php +++ /dev/null @@ -1,48 +0,0 @@ -app['feather.migrator'] = $this->app->share(function($app) - { - // Get the active migrator driver from the database. We'll then determine the connection to be used - // for the migrator. - $migrator = new Migrator; - - $migrator = $migrator->getMigratorDriver($app['config']->get('feather::migrator.driver')); - - // By default the database connection will be Feather's database connection. This is useful when you want - // the original tables from the old system to be on the same database as Feather. Migrators can overwrite - // this default connection. - $connection = $app['db']->connection(FEATHER_DATABASE); - - if ( ! is_null($migrator->options->database)) - { - $app['config']->set('database.connections.feather:migrator', array( - 'driver' => $migrator->options->driver, - 'host' => $migrator->options->host, - 'database' => $migrator->options->database, - 'username' => $migrator->options->username, - 'password' => $migrator->options->password, - 'charset' => $migrator->options->charset, - 'prefix' => $migrator->options->prefix, - 'collation' => $migrator->options->collation - )); - - $connection = $app['db']->connection('feather:migrator'); - } - - return new MigratorManager($app, $connection, ! is_null($migrator)); - }); - } - -} \ No newline at end of file diff --git a/feather/Presenter/Compilers/FeatherCompiler.php b/feather/Presenter/Compilers/FeatherCompiler.php deleted file mode 100644 index f60f042..0000000 --- a/feather/Presenter/Compilers/FeatherCompiler.php +++ /dev/null @@ -1,73 +0,0 @@ -compilers = array_merge($this->compilers, array( - 'Assignments', - 'ExtensionEvents', - 'InlineErrors', - 'Errors' - )); - - return parent::compileString($value); - } - - /** - * Compile assignments into valid PHP. - * - * @param string $value - * @return string - */ - public function compileAssignments($value) - { - return preg_replace('/(\s*)@assign\s*\(\$(.*), (.*)\)(\s*)/', '$1$4', $value); - } - - /** - * Compile extension events into valid PHP. - * - * @param string $value - * @return string - */ - public function compileExtensionEvents($value) - { - $pattern = $this->createMatcher('event'); - - return preg_replace($pattern, '$1', $value); - } - - /** - * Compile inline errors into valid PHP. - * - * @param string $value - * @return string - */ - public function compileInlineErrors($value) - { - $pattern = $this->createMatcher('error'); - - return preg_replace($pattern, '$1has$2 ? view("feather::errors.inline", array("error" => $errors->first$2)) : null; ?>', $value); - } - - /** - * Compile errors into valid PHP. - * - * @param string $value - * @return string - */ - public function compileErrors($value) - { - return str_replace('@errors', 'all() ? view("feather::errors.page", array("errors" => $errors->all())) : null; ?>', $value); - } - -} \ No newline at end of file diff --git a/feather/Presenter/Presenter.php b/feather/Presenter/Presenter.php deleted file mode 100644 index 4a60c37..0000000 --- a/feather/Presenter/Presenter.php +++ /dev/null @@ -1,54 +0,0 @@ -app = $app; - } - - /** - * Prepare the theme by requiring a starter file if it exists. - * - * @return void - */ - public function prepare() - { - // If we are within theme development mode and not running within the console then we'll publish the current themes - // assets. This is especially handy when you're making a lot of changes to your themes assets. - if ($this->app['config']->get('feather.forum.theme_development_mode') and ! $this->app->runningInConsole()) - { - $this->app['artisan']->call('feather:publish', array('name' => $this->app['config']->get('feather::forum.theme'), '--theme' => true)); - } - - // Assign a namespace and some cascading paths so that view files are first searched - // for within a theme then within the core view directory. - $this->app['view']->addLocation($this->app['path.themes'].'/'.$this->app['config']->get('feather.forum.theme').'/views'); - $this->app['view']->addLocation($this->app['path'].'/views'); - - // If the theme has a start file require the file to bootstrap the theme. - $start = $this->app['path.themes'].'/'.$this->app['config']->get('feather.forum.theme').'/start.php'; - - if ($this->app['files']->exists($start)) - { - // We'll make $app available to our start scripts so that they can access bound data and components. - $app = $this->app; - - require $start; - } - } - -} \ No newline at end of file diff --git a/feather/Presenter/PresenterServiceProvider.php b/feather/Presenter/PresenterServiceProvider.php deleted file mode 100644 index cb0e53a..0000000 --- a/feather/Presenter/PresenterServiceProvider.php +++ /dev/null @@ -1,47 +0,0 @@ -app['feather.presenter'] = $this->app->share(function($app) - { - return new Presenter($app); - }); - - $this->registerCompiler(); - } - - /** - * Register the view compiler. - * - * @return void - */ - public function registerCompiler() - { - // Our compiler needs an instance of the file system and the storage path of the compiled views. - $files = $this->app['files']; - - $storagePath = $this->app['path'].'/storage/views'; - - $this->app['view']->addExtension('blade.php', 'feather.compiler', function() use ($files, $storagePath) - { - // FeatherCompiler is used by Feather is an extension to the Blade compiler. Feather - // has a few special methods that are used throughout views that need to be compiled - // alongside the default Blade methods. - $compiler = new Compilers\FeatherCompiler($files, $storagePath); - - return new CompilerEngine($compiler, $files); - }); - } - -} \ No newline at end of file diff --git a/gears/docs/documentation/auth/singlesignon/introduction.md b/gears/docs/documentation/auth/singlesignon/introduction.md new file mode 100644 index 0000000..567dd0b --- /dev/null +++ b/gears/docs/documentation/auth/singlesignon/introduction.md @@ -0,0 +1,8 @@ +## Single Sign On + +One of Feather's design goals is to easily and effortlessly allow existing applications with an existing user base to integrate and interact with Feather. What this boils down to is a seemless transition from your application to the forums. No longer will your users have to remember two or more passwords for your application. + +Before using a single sign on provider there are a few things you must consider. + +1. If you've been maintaining a separate user base for your forum and your application for any period and have numerous users in both some users may be required to link there accounts on there initial visit to the forum. +2. A [forum migration](/migrations) and a single sign on provider can not be used in conjunction with each other. If you're migrating from an existing forum then it's recommended that you migrate users for a period of time before enabling single sign on. \ No newline at end of file diff --git a/gears/docs/documentation/auth/singlesignon/oneconnect.md b/gears/docs/documentation/auth/singlesignon/oneconnect.md new file mode 100644 index 0000000..d61532b --- /dev/null +++ b/gears/docs/documentation/auth/singlesignon/oneconnect.md @@ -0,0 +1,80 @@ +## OneConnect + +OneConnect is a gear developed for Feather which allows an application on the same domain as Feather to connect and share user information. OneConnect is very easy to get going, with only a few steps required on the developers end. + +To use OneConnect you need to have knowledge of how to fetch the logged in users information for your application and creating a page that can display this information. + +
    +**Hold up, are you using Laravel?** + +If you're developing an application with [Laravel](http://laravel.com) then we suggest you use the **[Harmonize](/auth/single-sign-on/harmonize)** gear instead. +
    + +### WordPress Plugin + +A WordPress plugin is provided with OneConnect. + +1. Copy the `feather-oneconnect.php` file from `gears/oneconnct/wordpress` into your WordPress plugin directory. +2. From your WordPress dashboard enable the plugin from the Installed Plugins screen. +3. Select the Feather OneConnect settings in the navigation and take note of the configuration details. +4. Enable OneConnect in your Feather dashboard, and during configuration paste the details that were listed by the WordPress plugin. + +Your WordPress users should now be able to login to your WordPress website and be automatically authorized on Feather. + +
    +Just a sec! Be sure to read the README.md file on setting the cookie domain and path for your WordPress installation. +
    + +### How OneConnect Works + +When a user logs in to your application a cookie is set for that user. Your application has access to this cookie, so on subsequent page visits the cookie is picked up and your application will know to keep that user logged in. Let's assume that **Jack** has just logged in to your application. When Jack visits the forum Feather will make a request to a page on your application (yes, the page you need to create). Because Jack is logged in this page shows some information about Jack that Feather grabs. Here's an example. + +~~~~ +{ + "id":874, + "username":"Jack", + "email":"jack@beanstalk.com" +} +~~~~ + +The response is JSON, this allows Feather to easily parse the users information. Now that Feather has the Jack's information it can attempt to authorize him on the forums. Feather will see that Jack is yet to visit the forums, so an account is created for Jack and it's linked to his account on your application. His experience is seemless and he knows nothing of what's going on in the background. + +If Jack has already visited the forums before then he'll be authorized for the forums. Again, Jack's experience is seemless. + +The only time Jack will have an interupted experience is when an account already exists with his username or e-mail address. The only time this may occur is if you're forum has been running longer then your application. If Feather is unable to authorize Jack he'll be presented with a screen asking him to create a new account or link to an existing account. If Jack was already signed up on your forums beforehand then he's able to provide his e-mail address on the forum and the corrosponding password and link his account. If Jack does not have an account then he's able to create a new account. + +OneConnect is actually quite a simple process, however for it to work there are a few things that you, as the developer, need to address on your application. + +### Application Cookies + +OneConnect requires that your applications cookies be set on the same domain as Feather. + +~~~~ +setcookie("TestCookie", $value, time() + 3600, "/", ".yourapp.com"); +~~~~ + +You can see that the cookie path and domain have been set to the entire domain, this means that OneConnect has access to your applications cookies. + +
    +Note that the leading **.** is required for older browsers still implementing the deprecated [RFC 2109](http://www.faqs.org/rfcs/rfc2109). +
    + +### User Information + +When setting up OneConnect you'll be required to enter an **Authentication URL**. This URL is where the JSON encoded logged in users information is displayed. The following fields are required for authorizing the user. + +~~~~ +{ + "id":1, + "username":"Username", + "email":"E-mail Address" +} +~~~~ + +
    +When a user is not logged in an empty JSON encoded array or blank page should be returned. +
    + +### Conclusion + +You should now have OneConnect configured and working correctly. When users from your application login they'll be automatically authoried on Feather. If you have any troubles try clearing your cookies first and then trying again. \ No newline at end of file diff --git a/gears/docs/documentation/gears/events.md b/gears/docs/documentation/gears/events.md new file mode 100644 index 0000000..4205870 --- /dev/null +++ b/gears/docs/documentation/gears/events.md @@ -0,0 +1,513 @@ +## Gear Events + +For those of you that use Laravel, Feather makes use of the Events system to power Gear events. Events are fired at points throughout the execution of Feather and your own custom events can be fired. + +### Event Naming Convention + +Feather uses a very simple naming convention for all events that are fired. This is an event in its most basic form. + + {category}: {timing} {event} + +An example of this convention is as follows. + + validation: before auth.register + +The above event is fired before the registrations validation as assessed. The above event is useful when you want your Gear to add custom inputs to the registration form. + +### Asset Events + + + + + + + + + + + + + + + + + +

    assets: change styles

    Receives + + + + + + + + + +
    ParameterDescription
    `object $container`Asset container being used by current theme.
    +
    Expects + `void` +
    + Fired after the controllers method has been run. An asset container for the theme is passed to the event allowing styles within the container to be modified. +
    + + + + + + + + + + + + + + + + + +

    assets: change scripts

    Receives + + + + + + + + + +
    ParameterDescription
    `object $container`Asset container being used by current theme.
    +
    Expects + `void` +
    + Fired after the controllers method has been run. An asset container for the theme is passed to the event allowing scripts within the container to be modified. +
    + +### Controller Events + + + + + + + + + + + + + + + + + +

    controller: before {controller.name}@{verb}.{method.name}

    Receives + + + + + + + + + +
    ParameterDescription
    `object $controller`Instance of the controller object being resolved.
    +
    Expects + `void` +
    + Fired before a given method on a given controller is executed. Listeners receive an instance of the controller object. +
    + + + + + + + + + + + + + + + + + +

    controller: after {controller.name}@{verb}.{method.name}

    Receives + + + + + + + + + +
    ParameterDescription
    `object $controller`Instance of the controller object being resolved.
    +
    Expects + `void` +
    + Fired after a given method on a given controller is executed. Listeners receive an instance of the controller object. +
    + + + + + + + + + + + + + + + + + +

    controller: override {controller.name}@{verb}.{method.name}

    Receives + + + + + + + + + +
    ParameterDescription
    `object $controller`Instance of the controller object being resolved.
    +
    Expects + `mixed` +
    + Overrides a given method on a given controller. No response needs to be returned meaning the event can interact with the `layout` property on the controller instance. +
    + + + + + + + + + + + + + + + + + +

    controller: create {controller.name}@{verb}.{method.name}

    Receives + + + + + + + + + +
    ParameterDescription
    `object $controller`Instance of the controller object being resolved.
    +
    Expects + `mixed` +
    + Create a method for the given HTTP verb on a given controller. As with overriding a method you can interact with the `layout` property on the controller instance. +
    + +
    +**Don't forget!** + +All controller events receive the controller instance meaning you can interact with the all methods and properties on that controller. +
    + +### View Events + + + + + + + + + + + + + +

    view: before category.title

    Expects + `mixed` +
    + Fired before the category title. Expects a value to be returned. +
    + + + + + + + + + + + + + +

    view: after category.title

    Expects + `mixed` +
    + Fired after the category title. Expects a value to be returned. +
    + + + + + + + + + + + + + +

    view: before category.discussion.counter

    Expects + `mixed` +
    + Fired before the category discussion counter. Expects a value to be returned. +
    + + + + + + + + + + + + + +

    view: after category.discussion.counter

    Expects + `mixed` +
    + Fired after the category discussion counter. Expects a value to be returned. +
    + + + + + + + + + + + + + +

    view: before category.discussion.title

    Expects + `mixed` +
    + Fired before the category discussion title. Expects a value to be returned. +
    + + + + + + + + + + + + + +

    view: after category.discussion.title

    Expects + `mixed` +
    + Fired after the category discussion title. Expects a value to be returned. +
    + + + + + + + + + + + + + +

    view: before category.discussion.meta

    Expects + `mixed` +
    + Fired before the category discussion meta information. Expects a value to be returned. +
    + + + + + + + + + + + + + +

    view: after category.discussion.meta

    Expects + `mixed` +
    + Fired after the category discussion meta information. Expects a value to be returned. +
    + + + + + + + + + + + + + +

    view: before category.discussion.stats

    Expects + `mixed` +
    + Fired before the category discussion statistics. Expects a value to be returned. +
    + + + + + + + + + + + + + +

    view: after category.discussion.stats

    Expects + `mixed` +
    + Fired after the category discussion statistics. Expects a value to be returned. +
    + + + + + + + + + + + + + +

    view: before template.title

    Expects + `mixed` +
    + Fired before the template title. Expects a value to be returned. +
    + + + + + + + + + + + + + +

    view: after template.title

    Expects + `mixed` +
    + Fired after the template title. Expects a value to be returned. +
    + + + + + + + + + + + + + +

    view: before template.meta

    Expects + `mixed` +
    + Fired before the template meta. Expects a value to be returned. +
    + + + + + + + + + + + + + +

    view: after template.meta

    Expects + `mixed` +
    + Fired after the template meta. Expects a value to be returned. +
    + + + + + + + + + + + + + +

    view: before register.rules

    Expects + `mixed` +
    + Fired before the registration rules. Expects a value to be returned. +
    + + + + + + + + + + + + + +

    view: after register.rules

    Expects + `mixed` +
    + Fired after the registration rules. Expects a value to be returned. +
    \ No newline at end of file diff --git a/gears/docs/documentation/home.md b/gears/docs/documentation/home.md new file mode 100644 index 0000000..a77c732 --- /dev/null +++ b/gears/docs/documentation/home.md @@ -0,0 +1,3 @@ +# Feather Forums + +Feather is a community forum system. \ No newline at end of file diff --git a/gears/docs/public/css/bootstrap.css b/gears/docs/public/css/bootstrap.css new file mode 100644 index 0000000..c1cc864 --- /dev/null +++ b/gears/docs/public/css/bootstrap.css @@ -0,0 +1,722 @@ +/*! + * Bootstrap v2.0.4 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ +.clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";} +.clearfix:after{clear:both;} +.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;} +.input-block-level{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;} +article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;} +audio,canvas,video{display:inline-block;*display:inline;*zoom:1;} +audio:not([controls]){display:none;} +html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;} +a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;} +a:hover,a:active{outline:0;} +sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline;} +sup{top:-0.5em;} +sub{bottom:-0.25em;} +img{max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic;} +#map_canvas img{max-width:none;} +button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;} +button,input{*overflow:visible;line-height:normal;} +button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0;} +button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;} +input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield;} +input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;} +textarea{overflow:auto;vertical-align:top;} +body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;color:#333333;background-color:#ffffff;} +a{color:#0088cc;text-decoration:none;} +a:hover{color:#005580;text-decoration:underline;} +.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";} +.row:after{clear:both;} +[class*="span"]{float:left;margin-left:20px;} +.container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;} +.span12{width:940px;} +.span11{width:860px;} +.span10{width:780px;} +.span9{width:700px;} +.span8{width:620px;} +.span7{width:540px;} +.span6{width:460px;} +.span5{width:380px;} +.span4{width:300px;} +.span3{width:220px;} +.span2{width:140px;} +.span1{width:60px;} +.offset12{margin-left:980px;} +.offset11{margin-left:900px;} +.offset10{margin-left:820px;} +.offset9{margin-left:740px;} +.offset8{margin-left:660px;} +.offset7{margin-left:580px;} +.offset6{margin-left:500px;} +.offset5{margin-left:420px;} +.offset4{margin-left:340px;} +.offset3{margin-left:260px;} +.offset2{margin-left:180px;} +.offset1{margin-left:100px;} +.row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} +.row-fluid:after{clear:both;} +.row-fluid [class*="span"]{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.127659574%;*margin-left:2.0744680846382977%;} +.row-fluid [class*="span"]:first-child{margin-left:0;} +.row-fluid .span12{width:99.99999998999999%;*width:99.94680850063828%;} +.row-fluid .span11{width:91.489361693%;*width:91.4361702036383%;} +.row-fluid .span10{width:82.97872339599999%;*width:82.92553190663828%;} +.row-fluid .span9{width:74.468085099%;*width:74.4148936096383%;} +.row-fluid .span8{width:65.95744680199999%;*width:65.90425531263828%;} +.row-fluid .span7{width:57.446808505%;*width:57.3936170156383%;} +.row-fluid .span6{width:48.93617020799999%;*width:48.88297871863829%;} +.row-fluid .span5{width:40.425531911%;*width:40.3723404216383%;} +.row-fluid .span4{width:31.914893614%;*width:31.8617021246383%;} +.row-fluid .span3{width:23.404255317%;*width:23.3510638276383%;} +.row-fluid .span2{width:14.89361702%;*width:14.8404255306383%;} +.row-fluid .span1{width:6.382978723%;*width:6.329787233638298%;} +.container{margin-right:auto;margin-left:auto;*zoom:1;}.container:before,.container:after{display:table;content:"";} +.container:after{clear:both;} +.container-fluid{padding-right:20px;padding-left:20px;*zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";} +.container-fluid:after{clear:both;} +p{margin:0 0 9px;}p small{font-size:11px;color:#999999;} +.lead{margin-bottom:18px;font-size:20px;font-weight:200;line-height:27px;} +h1,h2,h3,h4,h5,h6{margin:0;font-family:inherit;font-weight:bold;color:inherit;text-rendering:optimizelegibility;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;color:#999999;} +h1{font-size:30px;line-height:36px;}h1 small{font-size:18px;} +h2{font-size:24px;line-height:36px;}h2 small{font-size:18px;} +h3{font-size:18px;line-height:27px;}h3 small{font-size:14px;} +h4,h5,h6{line-height:18px;} +h4{font-size:14px;}h4 small{font-size:12px;} +h5{font-size:12px;} +h6{font-size:11px;color:#999999;text-transform:uppercase;} +.page-header{padding-bottom:17px;margin:18px 0;border-bottom:1px solid #eeeeee;} +.page-header h1{line-height:1;} +ul,ol{padding:0;margin:0 0 9px 25px;} +ul ul,ul ol,ol ol,ol ul{margin-bottom:0;} +ul{list-style:disc;} +ol{list-style:decimal;} +li{line-height:18px;} +ul.unstyled,ol.unstyled{margin-left:0;list-style:none;} +dl{margin-bottom:18px;} +dt,dd{line-height:18px;} +dt{font-weight:bold;line-height:17px;} +dd{margin-left:9px;} +.dl-horizontal dt{float:left;width:120px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} +.dl-horizontal dd{margin-left:130px;} +hr{margin:18px 0;border:0;border-top:1px solid #eeeeee;border-bottom:1px solid #ffffff;} +strong{font-weight:bold;} +em{font-style:italic;} +.muted{color:#999999;} +abbr[title]{cursor:help;border-bottom:1px dotted #999999;} +abbr.initialism{font-size:90%;text-transform:uppercase;} +blockquote{padding:0 0 0 15px;margin:0 0 18px;border-left:5px solid #eeeeee;}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:22.5px;} +blockquote small{display:block;line-height:18px;color:#999999;}blockquote small:before{content:'\2014 \00A0';} +blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eeeeee;border-left:0;}blockquote.pull-right p,blockquote.pull-right small{text-align:right;} +q:before,q:after,blockquote:before,blockquote:after{content:"";} +address{display:block;margin-bottom:18px;font-style:normal;line-height:18px;} +small{font-size:100%;} +cite{font-style:normal;} +code,pre{padding:0 3px 2px;font-family:Menlo,Monaco,Consolas,"Courier New",monospace;font-size:12px;color:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} +code{padding:2px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8;} +pre{display:block;padding:8.5px;margin:0 0 9px;font-size:12.025px;line-height:18px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}pre.prettyprint{margin-bottom:18px;} +pre code{padding:0;color:inherit;background-color:transparent;border:0;} +.pre-scrollable{max-height:340px;overflow-y:scroll;} +.label,.badge{font-size:10.998px;font-weight:bold;line-height:14px;color:#ffffff;vertical-align:baseline;white-space:nowrap;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#999999;} +.label{padding:1px 4px 2px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} +.badge{padding:1px 9px 2px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px;} +a.label:hover,a.badge:hover{color:#ffffff;text-decoration:none;cursor:pointer;} +.label-important,.badge-important{background-color:#b94a48;} +.label-important[href],.badge-important[href]{background-color:#953b39;} +.label-warning,.badge-warning{background-color:#f89406;} +.label-warning[href],.badge-warning[href]{background-color:#c67605;} +.label-success,.badge-success{background-color:#468847;} +.label-success[href],.badge-success[href]{background-color:#356635;} +.label-info,.badge-info{background-color:#3a87ad;} +.label-info[href],.badge-info[href]{background-color:#2d6987;} +.label-inverse,.badge-inverse{background-color:#333333;} +.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a;} +table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0;} +.table{width:100%;margin-bottom:18px;}.table th,.table td{padding:8px;line-height:18px;text-align:left;vertical-align:top;border-top:1px solid #dddddd;} +.table th{font-weight:bold;} +.table thead th{vertical-align:bottom;} +.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0;} +.table tbody+tbody{border-top:2px solid #dddddd;} +.table-condensed th,.table-condensed td{padding:4px 5px;} +.table-bordered{border:1px solid #dddddd;border-collapse:separate;*border-collapse:collapsed;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.table-bordered th,.table-bordered td{border-left:1px solid #dddddd;} +.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0;} +.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px;} +.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px;} +.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;} +.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;} +.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;} +.table tbody tr:hover td,.table tbody tr:hover th{background-color:#f5f5f5;} +table .span1{float:none;width:44px;margin-left:0;} +table .span2{float:none;width:124px;margin-left:0;} +table .span3{float:none;width:204px;margin-left:0;} +table .span4{float:none;width:284px;margin-left:0;} +table .span5{float:none;width:364px;margin-left:0;} +table .span6{float:none;width:444px;margin-left:0;} +table .span7{float:none;width:524px;margin-left:0;} +table .span8{float:none;width:604px;margin-left:0;} +table .span9{float:none;width:684px;margin-left:0;} +table .span10{float:none;width:764px;margin-left:0;} +table .span11{float:none;width:844px;margin-left:0;} +table .span12{float:none;width:924px;margin-left:0;} +table .span13{float:none;width:1004px;margin-left:0;} +table .span14{float:none;width:1084px;margin-left:0;} +table .span15{float:none;width:1164px;margin-left:0;} +table .span16{float:none;width:1244px;margin-left:0;} +table .span17{float:none;width:1324px;margin-left:0;} +table .span18{float:none;width:1404px;margin-left:0;} +table .span19{float:none;width:1484px;margin-left:0;} +table .span20{float:none;width:1564px;margin-left:0;} +table .span21{float:none;width:1644px;margin-left:0;} +table .span22{float:none;width:1724px;margin-left:0;} +table .span23{float:none;width:1804px;margin-left:0;} +table .span24{float:none;width:1884px;margin-left:0;} +form{margin:0 0 18px;} +fieldset{padding:0;margin:0;border:0;} +legend{display:block;width:100%;padding:0;margin-bottom:27px;font-size:19.5px;line-height:36px;color:#333333;border:0;border-bottom:1px solid #e5e5e5;}legend small{font-size:13.5px;color:#999999;} +label,input,button,select,textarea{font-size:13px;font-weight:normal;line-height:18px;} +input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;} +label{display:block;margin-bottom:5px;} +select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:18px;padding:4px;margin-bottom:9px;font-size:13px;line-height:18px;color:#555555;} +input,textarea{width:210px;} +textarea{height:auto;} +textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#ffffff;border:1px solid #cccccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-webkit-transition:border linear 0.2s,box-shadow linear 0.2s;-moz-transition:border linear 0.2s,box-shadow linear 0.2s;-ms-transition:border linear 0.2s,box-shadow linear 0.2s;-o-transition:border linear 0.2s,box-shadow linear 0.2s;transition:border linear 0.2s,box-shadow linear 0.2s;}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82, 168, 236, 0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);} +input[type="radio"],input[type="checkbox"]{margin:3px 0;*margin-top:0;line-height:normal;cursor:pointer;} +input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto;} +.uneditable-textarea{width:auto;height:auto;} +select,input[type="file"]{height:28px;*margin-top:4px;line-height:28px;} +select{width:220px;border:1px solid #bbb;} +select[multiple],select[size]{height:auto;} +select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;} +.radio,.checkbox{min-height:18px;padding-left:18px;} +.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px;} +.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px;} +.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle;} +.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px;} +.input-mini{width:60px;} +.input-small{width:90px;} +.input-medium{width:150px;} +.input-large{width:210px;} +.input-xlarge{width:270px;} +.input-xxlarge{width:530px;} +input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0;} +.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block;} +input,textarea,.uneditable-input{margin-left:0;} +input.span12, textarea.span12, .uneditable-input.span12{width:930px;} +input.span11, textarea.span11, .uneditable-input.span11{width:850px;} +input.span10, textarea.span10, .uneditable-input.span10{width:770px;} +input.span9, textarea.span9, .uneditable-input.span9{width:690px;} +input.span8, textarea.span8, .uneditable-input.span8{width:610px;} +input.span7, textarea.span7, .uneditable-input.span7{width:530px;} +input.span6, textarea.span6, .uneditable-input.span6{width:450px;} +input.span5, textarea.span5, .uneditable-input.span5{width:370px;} +input.span4, textarea.span4, .uneditable-input.span4{width:290px;} +input.span3, textarea.span3, .uneditable-input.span3{width:210px;} +input.span2, textarea.span2, .uneditable-input.span2{width:130px;} +input.span1, textarea.span1, .uneditable-input.span1{width:50px;} +input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eeeeee;border-color:#ddd;} +input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent;} +.control-group.warning>label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853;} +.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853;border-color:#c09853;}.control-group.warning .checkbox:focus,.control-group.warning .radio:focus,.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:0 0 6px #dbc59e;-moz-box-shadow:0 0 6px #dbc59e;box-shadow:0 0 6px #dbc59e;} +.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853;} +.control-group.error>label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48;} +.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48;border-color:#b94a48;}.control-group.error .checkbox:focus,.control-group.error .radio:focus,.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:0 0 6px #d59392;-moz-box-shadow:0 0 6px #d59392;box-shadow:0 0 6px #d59392;} +.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48;} +.control-group.success>label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847;} +.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847;border-color:#468847;}.control-group.success .checkbox:focus,.control-group.success .radio:focus,.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:0 0 6px #7aba7b;-moz-box-shadow:0 0 6px #7aba7b;box-shadow:0 0 6px #7aba7b;} +.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847;} +input:focus:required:invalid,textarea:focus:required:invalid,select:focus:required:invalid{color:#b94a48;border-color:#ee5f5b;}input:focus:required:invalid:focus,textarea:focus:required:invalid:focus,select:focus:required:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7;} +.form-actions{padding:17px 20px 18px;margin-top:18px;margin-bottom:18px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1;}.form-actions:before,.form-actions:after{display:table;content:"";} +.form-actions:after{clear:both;} +.uneditable-input{overflow:hidden;white-space:nowrap;cursor:not-allowed;background-color:#ffffff;border-color:#eee;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);} +:-moz-placeholder{color:#999999;} +:-ms-input-placeholder{color:#999999;} +::-webkit-input-placeholder{color:#999999;} +.help-block,.help-inline{color:#555555;} +.help-block{display:block;margin-bottom:9px;} +.help-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle;padding-left:5px;} +.input-prepend,.input-append{margin-bottom:5px;}.input-prepend input,.input-append input,.input-prepend select,.input-append select,.input-prepend .uneditable-input,.input-append .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:middle;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}.input-prepend input:focus,.input-append input:focus,.input-prepend select:focus,.input-append select:focus,.input-prepend .uneditable-input:focus,.input-append .uneditable-input:focus{z-index:2;} +.input-prepend .uneditable-input,.input-append .uneditable-input{border-left-color:#ccc;} +.input-prepend .add-on,.input-append .add-on{display:inline-block;width:auto;height:18px;min-width:16px;padding:4px 5px;font-weight:normal;line-height:18px;text-align:center;text-shadow:0 1px 0 #ffffff;vertical-align:middle;background-color:#eeeeee;border:1px solid #ccc;} +.input-prepend .add-on,.input-append .add-on,.input-prepend .btn,.input-append .btn{margin-left:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.input-prepend .active,.input-append .active{background-color:#a9dba9;border-color:#46a546;} +.input-prepend .add-on,.input-prepend .btn{margin-right:-1px;} +.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} +.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} +.input-append .uneditable-input{border-right-color:#ccc;border-left-color:#eee;} +.input-append .add-on:last-child,.input-append .btn:last-child{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;} +.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} +.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;} +.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px;} +.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;*zoom:1;margin-bottom:0;} +.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none;} +.form-search label,.form-inline label{display:inline-block;} +.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0;} +.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle;} +.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0;} +.control-group{margin-bottom:9px;} +legend+.control-group{margin-top:18px;-webkit-margin-top-collapse:separate;} +.form-horizontal .control-group{margin-bottom:18px;*zoom:1;}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;content:"";} +.form-horizontal .control-group:after{clear:both;} +.form-horizontal .control-label{float:left;width:140px;padding-top:5px;text-align:right;} +.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:160px;*margin-left:0;}.form-horizontal .controls:first-child{*padding-left:160px;} +.form-horizontal .help-block{margin-top:9px;margin-bottom:0;} +.form-horizontal .form-actions{padding-left:160px;} +.btn{display:inline-block;*display:inline;*zoom:1;padding:4px 10px 4px;margin-bottom:0;font-size:13px;line-height:18px;*line-height:20px;color:#333333;text-align:center;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;background-image:-moz-linear-gradient(top, #ffffff, #e6e6e6);background-image:-ms-linear-gradient(top, #ffffff, #e6e6e6);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(top, #ffffff, #e6e6e6);background-image:-o-linear-gradient(top, #ffffff, #e6e6e6);background-image:linear-gradient(top, #ffffff, #e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#e6e6e6;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border:1px solid #cccccc;*border:0;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*margin-left:.3em;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{background-color:#e6e6e6;*background-color:#d9d9d9;} +.btn:active,.btn.active{background-color:#cccccc \9;} +.btn:first-child{*margin-left:0;} +.btn:hover{color:#333333;text-decoration:none;background-color:#e6e6e6;*background-color:#d9d9d9;background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-ms-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear;} +.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;} +.btn.active,.btn:active{background-color:#e6e6e6;background-color:#d9d9d9 \9;background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05);} +.btn.disabled,.btn[disabled]{cursor:default;background-color:#e6e6e6;background-image:none;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} +.btn-large{padding:9px 14px;font-size:15px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;} +.btn-large [class^="icon-"]{margin-top:1px;} +.btn-small{padding:5px 9px;font-size:11px;line-height:16px;} +.btn-small [class^="icon-"]{margin-top:-1px;} +.btn-mini{padding:2px 6px;font-size:11px;line-height:14px;} +.btn-primary,.btn-primary:hover,.btn-warning,.btn-warning:hover,.btn-danger,.btn-danger:hover,.btn-success,.btn-success:hover,.btn-info,.btn-info:hover,.btn-inverse,.btn-inverse:hover{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);} +.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255, 255, 255, 0.75);} +.btn{border-color:#ccc;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);} +.btn-primary{background-color:#0074cc;background-image:-moz-linear-gradient(top, #0088cc, #0055cc);background-image:-ms-linear-gradient(top, #0088cc, #0055cc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0055cc));background-image:-webkit-linear-gradient(top, #0088cc, #0055cc);background-image:-o-linear-gradient(top, #0088cc, #0055cc);background-image:linear-gradient(top, #0088cc, #0055cc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0055cc', GradientType=0);border-color:#0055cc #0055cc #003580;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#0055cc;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{background-color:#0055cc;*background-color:#004ab3;} +.btn-primary:active,.btn-primary.active{background-color:#004099 \9;} +.btn-warning{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-ms-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(top, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#f89406;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{background-color:#f89406;*background-color:#df8505;} +.btn-warning:active,.btn-warning.active{background-color:#c67605 \9;} +.btn-danger{background-color:#da4f49;background-image:-moz-linear-gradient(top, #ee5f5b, #bd362f);background-image:-ms-linear-gradient(top, #ee5f5b, #bd362f);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));background-image:-webkit-linear-gradient(top, #ee5f5b, #bd362f);background-image:-o-linear-gradient(top, #ee5f5b, #bd362f);background-image:linear-gradient(top, #ee5f5b, #bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#bd362f;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{background-color:#bd362f;*background-color:#a9302a;} +.btn-danger:active,.btn-danger.active{background-color:#942a25 \9;} +.btn-success{background-color:#5bb75b;background-image:-moz-linear-gradient(top, #62c462, #51a351);background-image:-ms-linear-gradient(top, #62c462, #51a351);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));background-image:-webkit-linear-gradient(top, #62c462, #51a351);background-image:-o-linear-gradient(top, #62c462, #51a351);background-image:linear-gradient(top, #62c462, #51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#51a351;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{background-color:#51a351;*background-color:#499249;} +.btn-success:active,.btn-success.active{background-color:#408140 \9;} +.btn-info{background-color:#49afcd;background-image:-moz-linear-gradient(top, #5bc0de, #2f96b4);background-image:-ms-linear-gradient(top, #5bc0de, #2f96b4);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));background-image:-webkit-linear-gradient(top, #5bc0de, #2f96b4);background-image:-o-linear-gradient(top, #5bc0de, #2f96b4);background-image:linear-gradient(top, #5bc0de, #2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#2f96b4;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{background-color:#2f96b4;*background-color:#2a85a0;} +.btn-info:active,.btn-info.active{background-color:#24748c \9;} +.btn-inverse{background-color:#414141;background-image:-moz-linear-gradient(top, #555555, #222222);background-image:-ms-linear-gradient(top, #555555, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#555555), to(#222222));background-image:-webkit-linear-gradient(top, #555555, #222222);background-image:-o-linear-gradient(top, #555555, #222222);background-image:linear-gradient(top, #555555, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#555555', endColorstr='#222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#222222;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{background-color:#222222;*background-color:#151515;} +.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9;} +button.btn,input[type="submit"].btn{*padding-top:2px;*padding-bottom:2px;}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0;} +button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px;} +button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px;} +button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px;} +[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat;}[class^="icon-"]:last-child,[class*=" icon-"]:last-child{*margin-left:0;} +.icon-white{background-image:url("../img/glyphicons-halflings-white.png");} +.icon-glass{background-position:0 0;} +.icon-music{background-position:-24px 0;} +.icon-search{background-position:-48px 0;} +.icon-envelope{background-position:-72px 0;} +.icon-heart{background-position:-96px 0;} +.icon-star{background-position:-120px 0;} +.icon-star-empty{background-position:-144px 0;} +.icon-user{background-position:-168px 0;} +.icon-film{background-position:-192px 0;} +.icon-th-large{background-position:-216px 0;} +.icon-th{background-position:-240px 0;} +.icon-th-list{background-position:-264px 0;} +.icon-ok{background-position:-288px 0;} +.icon-remove{background-position:-312px 0;} +.icon-zoom-in{background-position:-336px 0;} +.icon-zoom-out{background-position:-360px 0;} +.icon-off{background-position:-384px 0;} +.icon-signal{background-position:-408px 0;} +.icon-cog{background-position:-432px 0;} +.icon-trash{background-position:-456px 0;} +.icon-home{background-position:0 -24px;} +.icon-file{background-position:-24px -24px;} +.icon-time{background-position:-48px -24px;} +.icon-road{background-position:-72px -24px;} +.icon-download-alt{background-position:-96px -24px;} +.icon-download{background-position:-120px -24px;} +.icon-upload{background-position:-144px -24px;} +.icon-inbox{background-position:-168px -24px;} +.icon-play-circle{background-position:-192px -24px;} +.icon-repeat{background-position:-216px -24px;} +.icon-refresh{background-position:-240px -24px;} +.icon-list-alt{background-position:-264px -24px;} +.icon-lock{background-position:-287px -24px;} +.icon-flag{background-position:-312px -24px;} +.icon-headphones{background-position:-336px -24px;} +.icon-volume-off{background-position:-360px -24px;} +.icon-volume-down{background-position:-384px -24px;} +.icon-volume-up{background-position:-408px -24px;} +.icon-qrcode{background-position:-432px -24px;} +.icon-barcode{background-position:-456px -24px;} +.icon-tag{background-position:0 -48px;} +.icon-tags{background-position:-25px -48px;} +.icon-book{background-position:-48px -48px;} +.icon-bookmark{background-position:-72px -48px;} +.icon-print{background-position:-96px -48px;} +.icon-camera{background-position:-120px -48px;} +.icon-font{background-position:-144px -48px;} +.icon-bold{background-position:-167px -48px;} +.icon-italic{background-position:-192px -48px;} +.icon-text-height{background-position:-216px -48px;} +.icon-text-width{background-position:-240px -48px;} +.icon-align-left{background-position:-264px -48px;} +.icon-align-center{background-position:-288px -48px;} +.icon-align-right{background-position:-312px -48px;} +.icon-align-justify{background-position:-336px -48px;} +.icon-list{background-position:-360px -48px;} +.icon-indent-left{background-position:-384px -48px;} +.icon-indent-right{background-position:-408px -48px;} +.icon-facetime-video{background-position:-432px -48px;} +.icon-picture{background-position:-456px -48px;} +.icon-pencil{background-position:0 -72px;} +.icon-map-marker{background-position:-24px -72px;} +.icon-adjust{background-position:-48px -72px;} +.icon-tint{background-position:-72px -72px;} +.icon-edit{background-position:-96px -72px;} +.icon-share{background-position:-120px -72px;} +.icon-check{background-position:-144px -72px;} +.icon-move{background-position:-168px -72px;} +.icon-step-backward{background-position:-192px -72px;} +.icon-fast-backward{background-position:-216px -72px;} +.icon-backward{background-position:-240px -72px;} +.icon-play{background-position:-264px -72px;} +.icon-pause{background-position:-288px -72px;} +.icon-stop{background-position:-312px -72px;} +.icon-forward{background-position:-336px -72px;} +.icon-fast-forward{background-position:-360px -72px;} +.icon-step-forward{background-position:-384px -72px;} +.icon-eject{background-position:-408px -72px;} +.icon-chevron-left{background-position:-432px -72px;} +.icon-chevron-right{background-position:-456px -72px;} +.icon-plus-sign{background-position:0 -96px;} +.icon-minus-sign{background-position:-24px -96px;} +.icon-remove-sign{background-position:-48px -96px;} +.icon-ok-sign{background-position:-72px -96px;} +.icon-question-sign{background-position:-96px -96px;} +.icon-info-sign{background-position:-120px -96px;} +.icon-screenshot{background-position:-144px -96px;} +.icon-remove-circle{background-position:-168px -96px;} +.icon-ok-circle{background-position:-192px -96px;} +.icon-ban-circle{background-position:-216px -96px;} +.icon-arrow-left{background-position:-240px -96px;} +.icon-arrow-right{background-position:-264px -96px;} +.icon-arrow-up{background-position:-289px -96px;} +.icon-arrow-down{background-position:-312px -96px;} +.icon-share-alt{background-position:-336px -96px;} +.icon-resize-full{background-position:-360px -96px;} +.icon-resize-small{background-position:-384px -96px;} +.icon-plus{background-position:-408px -96px;} +.icon-minus{background-position:-433px -96px;} +.icon-asterisk{background-position:-456px -96px;} +.icon-exclamation-sign{background-position:0 -120px;} +.icon-gift{background-position:-24px -120px;} +.icon-leaf{background-position:-48px -120px;} +.icon-fire{background-position:-72px -120px;} +.icon-eye-open{background-position:-96px -120px;} +.icon-eye-close{background-position:-120px -120px;} +.icon-warning-sign{background-position:-144px -120px;} +.icon-plane{background-position:-168px -120px;} +.icon-calendar{background-position:-192px -120px;} +.icon-random{background-position:-216px -120px;} +.icon-comment{background-position:-240px -120px;} +.icon-magnet{background-position:-264px -120px;} +.icon-chevron-up{background-position:-288px -120px;} +.icon-chevron-down{background-position:-313px -119px;} +.icon-retweet{background-position:-336px -120px;} +.icon-shopping-cart{background-position:-360px -120px;} +.icon-folder-close{background-position:-384px -120px;} +.icon-folder-open{background-position:-408px -120px;} +.icon-resize-vertical{background-position:-432px -119px;} +.icon-resize-horizontal{background-position:-456px -118px;} +.icon-hdd{background-position:0 -144px;} +.icon-bullhorn{background-position:-24px -144px;} +.icon-bell{background-position:-48px -144px;} +.icon-certificate{background-position:-72px -144px;} +.icon-thumbs-up{background-position:-96px -144px;} +.icon-thumbs-down{background-position:-120px -144px;} +.icon-hand-right{background-position:-144px -144px;} +.icon-hand-left{background-position:-168px -144px;} +.icon-hand-up{background-position:-192px -144px;} +.icon-hand-down{background-position:-216px -144px;} +.icon-circle-arrow-right{background-position:-240px -144px;} +.icon-circle-arrow-left{background-position:-264px -144px;} +.icon-circle-arrow-up{background-position:-288px -144px;} +.icon-circle-arrow-down{background-position:-312px -144px;} +.icon-globe{background-position:-336px -144px;} +.icon-wrench{background-position:-360px -144px;} +.icon-tasks{background-position:-384px -144px;} +.icon-filter{background-position:-408px -144px;} +.icon-briefcase{background-position:-432px -144px;} +.icon-fullscreen{background-position:-456px -144px;} +.btn-group{position:relative;*zoom:1;*margin-left:.3em;}.btn-group:before,.btn-group:after{display:table;content:"";} +.btn-group:after{clear:both;} +.btn-group:first-child{*margin-left:0;} +.btn-group+.btn-group{margin-left:5px;} +.btn-toolbar{margin-top:9px;margin-bottom:9px;}.btn-toolbar .btn-group{display:inline-block;*display:inline;*zoom:1;} +.btn-group>.btn{position:relative;float:left;margin-left:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.btn-group>.btn:first-child{margin-left:0;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;} +.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;} +.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px;} +.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;-moz-border-radius-topright:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;-moz-border-radius-bottomright:6px;border-bottom-right-radius:6px;} +.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2;} +.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0;} +.btn-group>.dropdown-toggle{padding-left:8px;padding-right:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,.125), inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,.125), inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);box-shadow:inset 1px 0 0 rgba(255,255,255,.125), inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);*padding-top:4px;*padding-bottom:4px;} +.btn-group>.btn-mini.dropdown-toggle{padding-left:5px;padding-right:5px;} +.btn-group>.btn-small.dropdown-toggle{*padding-top:4px;*padding-bottom:4px;} +.btn-group>.btn-large.dropdown-toggle{padding-left:12px;padding-right:12px;} +.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05);} +.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6;} +.btn-group.open .btn-primary.dropdown-toggle{background-color:#0055cc;} +.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406;} +.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f;} +.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351;} +.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4;} +.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222222;} +.btn .caret{margin-top:7px;margin-left:0;} +.btn:hover .caret,.open.btn-group .caret{opacity:1;filter:alpha(opacity=100);} +.btn-mini .caret{margin-top:5px;} +.btn-small .caret{margin-top:6px;} +.btn-large .caret{margin-top:6px;border-left-width:5px;border-right-width:5px;border-top-width:5px;} +.dropup .btn-large .caret{border-bottom:5px solid #000000;border-top:0;} +.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;opacity:0.75;filter:alpha(opacity=75);} +.nav{margin-left:0;margin-bottom:18px;list-style:none;} +.nav>li>a{display:block;} +.nav>li>a:hover{text-decoration:none;background-color:#eeeeee;} +.nav>.pull-right{float:right;} +.nav .nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:18px;color:#999999;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);text-transform:uppercase;} +.nav li+.nav-header{margin-top:9px;} +.nav-list{padding-left:15px;padding-right:15px;margin-bottom:0;} +.nav-list>li>a,.nav-list .nav-header{margin-left:-15px;margin-right:-15px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);} +.nav-list>li>a{padding:3px 15px;} +.nav-list>.active>a,.nav-list>.active>a:hover{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.2);background-color:#0088cc;} +.nav-list [class^="icon-"]{margin-right:2px;} +.nav-list .divider{*width:100%;height:1px;margin:8px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;} +.nav-tabs,.nav-pills{*zoom:1;}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;content:"";} +.nav-tabs:after,.nav-pills:after{clear:both;} +.nav-tabs>li,.nav-pills>li{float:left;} +.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px;} +.nav-tabs{border-bottom:1px solid #ddd;} +.nav-tabs>li{margin-bottom:-1px;} +.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:18px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #dddddd;} +.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555555;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;} +.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;} +.nav-pills>.active>a,.nav-pills>.active>a:hover{color:#ffffff;background-color:#0088cc;} +.nav-stacked>li{float:none;} +.nav-stacked>li>a{margin-right:0;} +.nav-tabs.nav-stacked{border-bottom:0;} +.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;} +.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;} +.nav-tabs.nav-stacked>li>a:hover{border-color:#ddd;z-index:2;} +.nav-pills.nav-stacked>li>a{margin-bottom:3px;} +.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px;} +.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 5px 5px;-moz-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px;} +.nav-pills .dropdown-menu{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.nav-tabs .dropdown-toggle .caret,.nav-pills .dropdown-toggle .caret{border-top-color:#0088cc;border-bottom-color:#0088cc;margin-top:6px;} +.nav-tabs .dropdown-toggle:hover .caret,.nav-pills .dropdown-toggle:hover .caret{border-top-color:#005580;border-bottom-color:#005580;} +.nav-tabs .active .dropdown-toggle .caret,.nav-pills .active .dropdown-toggle .caret{border-top-color:#333333;border-bottom-color:#333333;} +.nav>.dropdown.active>a:hover{color:#000000;cursor:pointer;} +.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover{color:#ffffff;background-color:#999999;border-color:#999999;} +.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;opacity:1;filter:alpha(opacity=100);} +.tabs-stacked .open>a:hover{border-color:#999999;} +.tabbable{*zoom:1;}.tabbable:before,.tabbable:after{display:table;content:"";} +.tabbable:after{clear:both;} +.tab-content{overflow:auto;} +.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0;} +.tab-content>.tab-pane,.pill-content>.pill-pane{display:none;} +.tab-content>.active,.pill-content>.active{display:block;} +.tabs-below>.nav-tabs{border-top:1px solid #ddd;} +.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0;} +.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}.tabs-below>.nav-tabs>li>a:hover{border-bottom-color:transparent;border-top-color:#ddd;} +.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover{border-color:transparent #ddd #ddd #ddd;} +.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none;} +.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px;} +.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd;} +.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px;} +.tabs-left>.nav-tabs>li>a:hover{border-color:#eeeeee #dddddd #eeeeee #eeeeee;} +.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#ffffff;} +.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd;} +.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0;} +.tabs-right>.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #eeeeee #dddddd;} +.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#ffffff;} +.navbar{*position:relative;*z-index:2;overflow:visible;margin-bottom:18px;} +.navbar-inner{min-height:40px;padding-left:20px;padding-right:20px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.25), inset 0 -1px 0 rgba(0,0,0,.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,.25), inset 0 -1px 0 rgba(0,0,0,.1);box-shadow:0 1px 3px rgba(0,0,0,.25), inset 0 -1px 0 rgba(0,0,0,.1);} +.navbar .container{width:auto;} +.nav-collapse.collapse{height:auto;} +.navbar{color:#999999;}.navbar .brand:hover{text-decoration:none;} +.navbar .brand{float:left;display:block;padding:8px 20px 12px;margin-left:-20px;font-size:20px;font-weight:200;line-height:1;color:#999999;} +.navbar .navbar-text{margin-bottom:0;line-height:40px;} +.navbar .navbar-link{color:#999999;}.navbar .navbar-link:hover{color:#ffffff;} +.navbar .btn,.navbar .btn-group{margin-top:5px;} +.navbar .btn-group .btn{margin:0;} +.navbar-form{margin-bottom:0;*zoom:1;}.navbar-form:before,.navbar-form:after{display:table;content:"";} +.navbar-form:after{clear:both;} +.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px;} +.navbar-form input,.navbar-form select{display:inline-block;margin-bottom:0;} +.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px;} +.navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap;}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0;} +.navbar-search{position:relative;float:left;margin-top:6px;margin-bottom:0;}.navbar-search .search-query{padding:4px 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;color:#ffffff;background-color:#626262;border:1px solid #151515;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.15);box-shadow:inset 0 1px 2px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.15);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.navbar-search .search-query:-moz-placeholder{color:#cccccc;} +.navbar-search .search-query:-ms-input-placeholder{color:#cccccc;} +.navbar-search .search-query::-webkit-input-placeholder{color:#cccccc;} +.navbar-search .search-query:focus,.navbar-search .search-query.focused{padding:5px 10px;color:#333333;text-shadow:0 1px 0 #ffffff;background-color:#ffffff;border:0;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);outline:0;} +.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0;} +.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-left:0;padding-right:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;} +.navbar-fixed-top{top:0;} +.navbar-fixed-bottom{bottom:0;} +.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0;} +.navbar .nav.pull-right{float:right;} +.navbar .nav>li{display:block;float:left;} +.navbar .nav>li>a{float:none;padding:9px 10px 11px;line-height:19px;color:#999999;text-decoration:none;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);} +.navbar .btn{display:inline-block;padding:4px 10px 4px;margin:5px 5px 6px;line-height:18px;} +.navbar .btn-group{margin:0;padding:5px 5px 6px;} +.navbar .nav>li>a:hover{background-color:transparent;color:#ffffff;text-decoration:none;} +.navbar .nav .active>a,.navbar .nav .active>a:hover{color:#ffffff;text-decoration:none;background-color:#222222;} +.navbar .divider-vertical{height:40px;width:1px;margin:0 9px;overflow:hidden;background-color:#222222;border-right:1px solid #333333;} +.navbar .nav.pull-right{margin-left:10px;margin-right:0;} +.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-left:5px;margin-right:5px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#222222;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075);}.navbar .btn-navbar:hover,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{background-color:#222222;*background-color:#151515;} +.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#080808 \9;} +.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);} +.btn-navbar .icon-bar+.icon-bar{margin-top:3px;} +.navbar .dropdown-menu:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0, 0, 0, 0.2);position:absolute;top:-7px;left:9px;} +.navbar .dropdown-menu:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #ffffff;position:absolute;top:-6px;left:10px;} +.navbar-fixed-bottom .dropdown-menu:before{border-top:7px solid #ccc;border-top-color:rgba(0, 0, 0, 0.2);border-bottom:0;bottom:-7px;top:auto;} +.navbar-fixed-bottom .dropdown-menu:after{border-top:6px solid #ffffff;border-bottom:0;bottom:-6px;top:auto;} +.navbar .nav li.dropdown .dropdown-toggle .caret,.navbar .nav li.dropdown.open .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;} +.navbar .nav li.dropdown.active .caret{opacity:1;filter:alpha(opacity=100);} +.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{background-color:transparent;} +.navbar .nav li.dropdown.active>.dropdown-toggle:hover{color:#ffffff;} +.navbar .pull-right .dropdown-menu,.navbar .dropdown-menu.pull-right{left:auto;right:0;}.navbar .pull-right .dropdown-menu:before,.navbar .dropdown-menu.pull-right:before{left:auto;right:12px;} +.navbar .pull-right .dropdown-menu:after,.navbar .dropdown-menu.pull-right:after{left:auto;right:13px;} +.breadcrumb{padding:7px 14px;margin:0 0 18px;list-style:none;background-color:#fbfbfb;background-image:-moz-linear-gradient(top, #ffffff, #f5f5f5);background-image:-ms-linear-gradient(top, #ffffff, #f5f5f5);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5));background-image:-webkit-linear-gradient(top, #ffffff, #f5f5f5);background-image:-o-linear-gradient(top, #ffffff, #f5f5f5);background-image:linear-gradient(top, #ffffff, #f5f5f5);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0);border:1px solid #ddd;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;}.breadcrumb li{display:inline-block;*display:inline;*zoom:1;text-shadow:0 1px 0 #ffffff;} +.breadcrumb .divider{padding:0 5px;color:#999999;} +.breadcrumb .active a{color:#333333;} +.pagination{height:36px;margin:18px 0;} +.pagination ul{display:inline-block;*display:inline;*zoom:1;margin-left:0;margin-bottom:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);} +.pagination li{display:inline;} +.pagination a{float:left;padding:0 14px;line-height:34px;text-decoration:none;border:1px solid #ddd;border-left-width:0;} +.pagination a:hover,.pagination .active a{background-color:#f5f5f5;} +.pagination .active a{color:#999999;cursor:default;} +.pagination .disabled span,.pagination .disabled a,.pagination .disabled a:hover{color:#999999;background-color:transparent;cursor:default;} +.pagination li:first-child a{border-left-width:1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} +.pagination li:last-child a{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;} +.pagination-centered{text-align:center;} +.pagination-right{text-align:right;} +.pager{margin-left:0;margin-bottom:18px;list-style:none;text-align:center;*zoom:1;}.pager:before,.pager:after{display:table;content:"";} +.pager:after{clear:both;} +.pager li{display:inline;} +.pager a{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;} +.pager a:hover{text-decoration:none;background-color:#f5f5f5;} +.pager .next a{float:right;} +.pager .previous a{float:left;} +.pager .disabled a,.pager .disabled a:hover{color:#999999;background-color:#fff;cursor:default;} +.thumbnails{margin-left:-20px;list-style:none;*zoom:1;}.thumbnails:before,.thumbnails:after{display:table;content:"";} +.thumbnails:after{clear:both;} +.row-fluid .thumbnails{margin-left:0;} +.thumbnails>li{float:left;margin-bottom:18px;margin-left:20px;} +.thumbnail{display:block;padding:4px;line-height:1;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);} +a.thumbnail:hover{border-color:#0088cc;-webkit-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);-moz-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);} +.thumbnail>img{display:block;max-width:100%;margin-left:auto;margin-right:auto;} +.thumbnail .caption{padding:9px;} +.alert{padding:8px 35px 8px 14px;margin-bottom:18px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;color:#c09853;} +.alert-heading{color:inherit;} +.alert .close{position:relative;top:-2px;right:-21px;line-height:18px;} +.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#468847;} +.alert-danger,.alert-error{background-color:#f2dede;border-color:#eed3d7;color:#b94a48;} +.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#3a87ad;} +.alert-block{padding-top:14px;padding-bottom:14px;} +.alert-block>p,.alert-block>ul{margin-bottom:0;} +.alert-block p+p{margin-top:5px;} +@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-o-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}.progress{overflow:hidden;height:18px;margin-bottom:18px;background-color:#f7f7f7;background-image:-moz-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-ms-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));background-image:-webkit-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-o-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:linear-gradient(top, #f5f5f5, #f9f9f9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.progress .bar{width:0%;height:18px;color:#ffffff;font-size:12px;text-align:center;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top, #149bdf, #0480be);background-image:-ms-linear-gradient(top, #149bdf, #0480be);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));background-image:-webkit-linear-gradient(top, #149bdf, #0480be);background-image:-o-linear-gradient(top, #149bdf, #0480be);background-image:linear-gradient(top, #149bdf, #0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width 0.6s ease;-moz-transition:width 0.6s ease;-ms-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease;} +.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px;} +.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite;} +.progress-danger .bar{background-color:#dd514c;background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-ms-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(top, #ee5f5b, #c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);} +.progress-danger.progress-striped .bar{background-color:#ee5f5b;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} +.progress-success .bar{background-color:#5eb95e;background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-ms-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(top, #62c462, #57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);} +.progress-success.progress-striped .bar{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} +.progress-info .bar{background-color:#4bb1cf;background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-ms-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(top, #5bc0de, #339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);} +.progress-info.progress-striped .bar{background-color:#5bc0de;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} +.progress-warning .bar{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-ms-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(top, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);} +.progress-warning.progress-striped .bar{background-color:#fbb450;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} +.hero-unit{padding:60px;margin-bottom:30px;background-color:#eeeeee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;color:inherit;letter-spacing:-1px;} +.hero-unit p{font-size:18px;font-weight:200;line-height:27px;color:inherit;} +.tooltip{position:absolute;z-index:1020;display:block;visibility:visible;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);}.tooltip.in{opacity:0.8;filter:alpha(opacity=80);} +.tooltip.top{margin-top:-2px;} +.tooltip.right{margin-left:2px;} +.tooltip.bottom{margin-top:2px;} +.tooltip.left{margin-left:-2px;} +.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;} +.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;} +.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;} +.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;} +.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;text-decoration:none;background-color:#000000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.tooltip-arrow{position:absolute;width:0;height:0;} +.popover{position:absolute;top:0;left:0;z-index:1010;display:none;padding:5px;}.popover.top{margin-top:-5px;} +.popover.right{margin-left:5px;} +.popover.bottom{margin-top:5px;} +.popover.left{margin-left:-5px;} +.popover.top .arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;} +.popover.right .arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;} +.popover.bottom .arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;} +.popover.left .arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;} +.popover .arrow{position:absolute;width:0;height:0;} +.popover-inner{padding:3px;width:280px;overflow:hidden;background:#000000;background:rgba(0, 0, 0, 0.8);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);} +.popover-title{padding:9px 15px;line-height:1;background-color:#f5f5f5;border-bottom:1px solid #eee;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;} +.popover-content{padding:14px;background-color:#ffffff;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.popover-content p,.popover-content ul,.popover-content ol{margin-bottom:0;} +.modal-open .dropdown-menu{z-index:2050;} +.modal-open .dropdown.open{*z-index:2050;} +.modal-open .popover{z-index:2060;} +.modal-open .tooltip{z-index:2070;} +.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000;}.modal-backdrop.fade{opacity:0;} +.modal-backdrop,.modal-backdrop.fade.in{opacity:0.8;filter:alpha(opacity=80);} +.modal{position:fixed;top:50%;left:50%;z-index:1050;overflow:auto;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-ms-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;} +.modal.fade.in{top:50%;} +.modal-header{padding:9px 15px;border-bottom:1px solid #eee;}.modal-header .close{margin-top:2px;} +.modal-body{overflow-y:auto;max-height:400px;padding:15px;} +.modal-form{margin-bottom:0;} +.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;*zoom:1;}.modal-footer:before,.modal-footer:after{display:table;content:"";} +.modal-footer:after{clear:both;} +.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0;} +.modal-footer .btn-group .btn+.btn{margin-left:-1px;} +.dropup,.dropdown{position:relative;} +.dropdown-toggle{*margin-bottom:-3px;} +.dropdown-toggle:active,.open .dropdown-toggle{outline:0;} +.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000000;border-right:4px solid transparent;border-left:4px solid transparent;content:"";opacity:0.3;filter:alpha(opacity=30);} +.dropdown .caret{margin-top:8px;margin-left:2px;} +.dropdown:hover .caret,.open .caret{opacity:1;filter:alpha(opacity=100);} +.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:4px 0;margin:1px 0 0;list-style:none;background-color:#ffffff;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-webkit-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;}.dropdown-menu.pull-right{right:0;left:auto;} +.dropdown-menu .divider{*width:100%;height:1px;margin:8px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;} +.dropdown-menu a{display:block;padding:3px 15px;clear:both;font-weight:normal;line-height:18px;color:#333333;white-space:nowrap;} +.dropdown-menu li>a:hover,.dropdown-menu .active>a,.dropdown-menu .active>a:hover{color:#ffffff;text-decoration:none;background-color:#0088cc;} +.open{*z-index:1000;}.open >.dropdown-menu{display:block;} +.pull-right>.dropdown-menu{right:0;left:auto;} +.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000000;content:"\2191";} +.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px;} +.typeahead{margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.accordion{margin-bottom:18px;} +.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.accordion-heading{border-bottom:0;} +.accordion-heading .accordion-toggle{display:block;padding:8px 15px;} +.accordion-toggle{cursor:pointer;} +.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5;} +.carousel{position:relative;margin-bottom:18px;line-height:1;} +.carousel-inner{overflow:hidden;width:100%;position:relative;} +.carousel .item{display:none;position:relative;-webkit-transition:0.6s ease-in-out left;-moz-transition:0.6s ease-in-out left;-ms-transition:0.6s ease-in-out left;-o-transition:0.6s ease-in-out left;transition:0.6s ease-in-out left;} +.carousel .item>img{display:block;line-height:1;} +.carousel .active,.carousel .next,.carousel .prev{display:block;} +.carousel .active{left:0;} +.carousel .next,.carousel .prev{position:absolute;top:0;width:100%;} +.carousel .next{left:100%;} +.carousel .prev{left:-100%;} +.carousel .next.left,.carousel .prev.right{left:0;} +.carousel .active.left{left:-100%;} +.carousel .active.right{left:100%;} +.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#ffffff;text-align:center;background:#222222;border:3px solid #ffffff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:0.5;filter:alpha(opacity=50);}.carousel-control.right{left:auto;right:15px;} +.carousel-control:hover{color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90);} +.carousel-caption{position:absolute;left:0;right:0;bottom:0;padding:10px 15px 5px;background:#333333;background:rgba(0, 0, 0, 0.75);} +.carousel-caption h4,.carousel-caption p{color:#ffffff;} +.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #eee;border:1px solid rgba(0, 0, 0, 0.05);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);}.well blockquote{border-color:#ddd;border-color:rgba(0, 0, 0, 0.15);} +.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;} +.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} +.close{float:right;font-size:20px;font-weight:bold;line-height:18px;color:#000000;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20);}.close:hover{color:#000000;text-decoration:none;cursor:pointer;opacity:0.4;filter:alpha(opacity=40);} +button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none;} +.pull-right{float:right;} +.pull-left{float:left;} +.hide{display:none;} +.show{display:block;} +.invisible{visibility:hidden;} +.fade{opacity:0;-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-ms-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;}.fade.in{opacity:1;} +.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height 0.35s ease;-moz-transition:height 0.35s ease;-ms-transition:height 0.35s ease;-o-transition:height 0.35s ease;transition:height 0.35s ease;}.collapse.in{height:auto;} diff --git a/gears/docs/public/css/docs.css b/gears/docs/public/css/docs.css new file mode 100644 index 0000000..c066126 --- /dev/null +++ b/gears/docs/public/css/docs.css @@ -0,0 +1,155 @@ +body { + padding-top: 60px; + line-height: 1.6em; + padding-bottom: 20px; +} + +li { + line-height: 1.6em; +} + +p:last-child { + margin-bottom: 0; +} + +.sidebar-nav { + padding: 8px; +} + +pre { + color: #333333; + display: block; + font-family: Menlo,Monaco,"Courier New",monospace; + font-size: 12px; + line-height: 18px; +} + +.prettyprint { + margin-bottom: 24px; +} + +.com { + color: #5A525F; +} + +.lit { + color: #195F91; +} + +.pun, .opn, .clo { + color: #93A1A1; +} + +.fun { + color: #DC322F; +} + +.str, .atv { + color: #D01D33; +} + +.kwd, .linenums .tag { + color: #142459; +} + +.typ, .atn, .dec, .var { + color: #947897; +} + +.pln { + color: #666666; +} + +.prettyprint { + background-color: #F9F9F9; + border: 1px solid #F2F2F2; + border-radius: 2px 2px 2px 2px; + max-height: 300px; + overflow: auto; + padding: 20px; +} + +.prettyprint.linenums { + box-shadow: 70px 0 0 #FFFFFF inset, 71px 0 0 #F1F1F1 inset; +} + +ol.linenums { + margin: 0 0 0 50px; +} + +ol.linenums li { + color: #BEBEC5; + line-height: 21px; + margin: 0; + padding-left: 30px; + text-shadow: 0 1px 0 #FFFFFF; +} + +ol.linenums li:hover { + background-color: #F3F3F3; +} + +.prettyprint.linenums { + box-shadow: none !important; + -moz-box-shadow: none !important; + -webkit-box-shadow: none !important; +} + +.prettyprint.linenums li { + color: #F9F9F9 !important; + padding-left: 0 !important; + font-size: 12px !important; +} + +ol.linenums { + margin: 0 !important; +} + +.api { + width: 100%; + background-color: #F9F9F9; + border: 1px solid #e8e8e8; + margin: 18px 0 18px; +} + +.api:last-child { + margin: 0; +} + +.api th { + padding: 10px; + border-bottom: 1px solid #e8e8e8; + text-align: left; +} + +.api .api-label { + background-color: #f5f5f5; + vertical-align: top; + font-weight: bold; + border-right: 1px solid #e8e8e8; +} + +.api td { + padding: 10px; +} + +.api .parameters th { + text-transform: uppercase; + color: #777; + border-bottom: 0; + padding: 0; +} + +.api .parameters td { + padding: 0 48px 0 0; +} + +.api .parameter { + display: pre; + color: #333; + font-family: Menlo,Monaco,Consolas,"Courier New",monospace; +} + +.api .description { + border-top: 1px solid #e8e8e8; +} \ No newline at end of file diff --git a/gears/docs/public/img/glyphicons-halflings-white.png b/gears/docs/public/img/glyphicons-halflings-white.png new file mode 100644 index 0000000..3bf6484 Binary files /dev/null and b/gears/docs/public/img/glyphicons-halflings-white.png differ diff --git a/gears/docs/public/img/glyphicons-halflings.png b/gears/docs/public/img/glyphicons-halflings.png new file mode 100644 index 0000000..79bc568 Binary files /dev/null and b/gears/docs/public/img/glyphicons-halflings.png differ diff --git a/gears/docs/public/js/bootstrap.js b/gears/docs/public/js/bootstrap.js new file mode 100644 index 0000000..2c00b6f --- /dev/null +++ b/gears/docs/public/js/bootstrap.js @@ -0,0 +1,7 @@ +/** +* Bootstrap.js by @fat & @mdo +* plugins: bootstrap-transition.js, bootstrap-modal.js, bootstrap-dropdown.js, bootstrap-scrollspy.js, bootstrap-tab.js, bootstrap-tooltip.js, bootstrap-popover.js, bootstrap-alert.js, bootstrap-button.js, bootstrap-collapse.js, bootstrap-carousel.js, bootstrap-typeahead.js +* Copyright 2012 Twitter, Inc. +* http://www.apache.org/licenses/LICENSE-2.0.txt +*/ +!function(a){a(function(){a.support.transition=function(){var a=function(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",msTransition:"MSTransitionEnd",transition:"transitionend"},c;for(c in b)if(a.style[c]!==undefined)return b[c]}();return a&&{end:a}}()})}(window.jQuery),!function(a){function c(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),d.call(b)},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),d.call(b)})}function d(a){this.$element.hide().trigger("hidden"),e.call(this)}function e(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('