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.
-
-[](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 }}
+
+
\ 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 }}
+
+
\ 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.
+
+
+@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)
+
+
+
+ @foreach($place->children as $key => $child)
+
+ {{-- Only show the children up to the 5th level, 3 levels of dropdown. --}}
+ @if($child->depth + $difference < 5)
+
+ {{ ($child->depth + $difference > $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. --}}
+
+ @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
+
\ 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 @@
+
\ 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::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::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) . "{$element}>";
+ }
+
+}
\ 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
+
+
+
+
Parameter
+
Description
+
+
+
`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
+
+
+
+
Parameter
+
Description
+
+
+
`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
+
+
+
+
Parameter
+
Description
+
+
+
`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
+
+
+
+
Parameter
+
Description
+
+
+
`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.
+
+ 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.
+
+ 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.
+
define style rules. See the example page for examples.
+ *
mark the {@code
} and {@code } tags in your source with
+ * {@code class=prettyprint.}
+ * You can also use the (html deprecated) {@code } tag, but the pretty
+ * printer needs to do more substantial DOM manipulations to support that, so
+ * some css styles may not be preserved.
+ *
+ * That's it. I wanted to keep the API as simple as possible, so there's no
+ * need to specify which language the code is in, but if you wish, you can add
+ * another class to the {@code
} or {@code } element to specify the
+ * language, as in {@code
}. Any class that
+ * starts with "lang-" followed by a file extension, specifies the file type.
+ * See the "lang-*.js" files in this directory for code that implements
+ * per-language file handlers.
+ *
+ * Change log:
+ * cbeust, 2006/08/22
+ *
+ * Java annotations (start with "@") are now captured as literals ("lit")
+ *
+ * @requires console
+ */
+
+// JSLint declarations
+/*global console, document, navigator, setTimeout, window */
+
+/**
+ * Split {@code prettyPrint} into multiple timeouts so as not to interfere with
+ * UI events.
+ * If set to {@code false}, {@code prettyPrint()} is synchronous.
+ */
+window['PR_SHOULD_USE_CONTINUATION'] = true;
+
+(function () {
+ // Keyword lists for various languages.
+ // We use things that coerce to strings to make them compact when minified
+ // and to defeat aggressive optimizers that fold large string constants.
+ var FLOW_CONTROL_KEYWORDS = ["break,continue,do,else,for,if,return,while"];
+ var C_KEYWORDS = [FLOW_CONTROL_KEYWORDS,"auto,case,char,const,default," +
+ "double,enum,extern,float,goto,int,long,register,short,signed,sizeof," +
+ "static,struct,switch,typedef,union,unsigned,void,volatile"];
+ var COMMON_KEYWORDS = [C_KEYWORDS,"catch,class,delete,false,import," +
+ "new,operator,private,protected,public,this,throw,true,try,typeof"];
+ var CPP_KEYWORDS = [COMMON_KEYWORDS,"alignof,align_union,asm,axiom,bool," +
+ "concept,concept_map,const_cast,constexpr,decltype," +
+ "dynamic_cast,explicit,export,friend,inline,late_check," +
+ "mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast," +
+ "template,typeid,typename,using,virtual,where"];
+ var JAVA_KEYWORDS = [COMMON_KEYWORDS,
+ "abstract,boolean,byte,extends,final,finally,implements,import," +
+ "instanceof,null,native,package,strictfp,super,synchronized,throws," +
+ "transient"];
+ var CSHARP_KEYWORDS = [JAVA_KEYWORDS,
+ "as,base,by,checked,decimal,delegate,descending,dynamic,event," +
+ "fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock," +
+ "object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed," +
+ "stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];
+ var COFFEE_KEYWORDS = "all,and,by,catch,class,else,extends,false,finally," +
+ "for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then," +
+ "true,try,unless,until,when,while,yes";
+ var JSCRIPT_KEYWORDS = [COMMON_KEYWORDS,
+ "debugger,eval,export,function,get,null,set,undefined,var,with," +
+ "Infinity,NaN"];
+ var PERL_KEYWORDS = "caller,delete,die,do,dump,elsif,eval,exit,foreach,for," +
+ "goto,if,import,last,local,my,next,no,our,print,package,redo,require," +
+ "sub,undef,unless,until,use,wantarray,while,BEGIN,END";
+ var PYTHON_KEYWORDS = [FLOW_CONTROL_KEYWORDS, "and,as,assert,class,def,del," +
+ "elif,except,exec,finally,from,global,import,in,is,lambda," +
+ "nonlocal,not,or,pass,print,raise,try,with,yield," +
+ "False,True,None"];
+ var RUBY_KEYWORDS = [FLOW_CONTROL_KEYWORDS, "alias,and,begin,case,class," +
+ "def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo," +
+ "rescue,retry,self,super,then,true,undef,unless,until,when,yield," +
+ "BEGIN,END"];
+ var SH_KEYWORDS = [FLOW_CONTROL_KEYWORDS, "case,done,elif,esac,eval,fi," +
+ "function,in,local,set,then,until"];
+ var ALL_KEYWORDS = [
+ CPP_KEYWORDS, CSHARP_KEYWORDS, JSCRIPT_KEYWORDS, PERL_KEYWORDS +
+ PYTHON_KEYWORDS, RUBY_KEYWORDS, SH_KEYWORDS];
+ var C_TYPES = /^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;
+
+ // token style names. correspond to css classes
+ /**
+ * token style for a string literal
+ * @const
+ */
+ var PR_STRING = 'str';
+ /**
+ * token style for a keyword
+ * @const
+ */
+ var PR_KEYWORD = 'kwd';
+ /**
+ * token style for a comment
+ * @const
+ */
+ var PR_COMMENT = 'com';
+ /**
+ * token style for a type
+ * @const
+ */
+ var PR_TYPE = 'typ';
+ /**
+ * token style for a literal value. e.g. 1, null, true.
+ * @const
+ */
+ var PR_LITERAL = 'lit';
+ /**
+ * token style for a punctuation string.
+ * @const
+ */
+ var PR_PUNCTUATION = 'pun';
+ /**
+ * token style for a punctuation string.
+ * @const
+ */
+ var PR_PLAIN = 'pln';
+
+ /**
+ * token style for an sgml tag.
+ * @const
+ */
+ var PR_TAG = 'tag';
+ /**
+ * token style for a markup declaration such as a DOCTYPE.
+ * @const
+ */
+ var PR_DECLARATION = 'dec';
+ /**
+ * token style for embedded source.
+ * @const
+ */
+ var PR_SOURCE = 'src';
+ /**
+ * token style for an sgml attribute name.
+ * @const
+ */
+ var PR_ATTRIB_NAME = 'atn';
+ /**
+ * token style for an sgml attribute value.
+ * @const
+ */
+ var PR_ATTRIB_VALUE = 'atv';
+
+ /**
+ * A class that indicates a section of markup that is not code, e.g. to allow
+ * embedding of line numbers within code listings.
+ * @const
+ */
+ var PR_NOCODE = 'nocode';
+
+
+
+/**
+ * A set of tokens that can precede a regular expression literal in
+ * javascript
+ * http://web.archive.org/web/20070717142515/http://www.mozilla.org/js/language/js20/rationale/syntax.html
+ * has the full list, but I've removed ones that might be problematic when
+ * seen in languages that don't support regular expression literals.
+ *
+ *
Specifically, I've removed any keywords that can't precede a regexp
+ * literal in a syntactically legal javascript program, and I've removed the
+ * "in" keyword since it's not a keyword in many languages, and might be used
+ * as a count of inches.
+ *
+ *
The link a above does not accurately describe EcmaScript rules since
+ * it fails to distinguish between (a=++/b/i) and (a++/b/i) but it works
+ * very well in practice.
+ *
+ * @private
+ * @const
+ */
+var REGEXP_PRECEDER_PATTERN = '(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*';
+
+// CAVEAT: this does not properly handle the case where a regular
+// expression immediately follows another since a regular expression may
+// have flags for case-sensitivity and the like. Having regexp tokens
+// adjacent is not valid in any language I'm aware of, so I'm punting.
+// TODO: maybe style special characters inside a regexp as punctuation.
+
+
+ /**
+ * Given a group of {@link RegExp}s, returns a {@code RegExp} that globally
+ * matches the union of the sets of strings matched by the input RegExp.
+ * Since it matches globally, if the input strings have a start-of-input
+ * anchor (/^.../), it is ignored for the purposes of unioning.
+ * @param {Array.} regexs non multiline, non-global regexs.
+ * @return {RegExp} a global regex.
+ */
+ function combinePrefixPatterns(regexs) {
+ var capturedGroupIndex = 0;
+
+ var needToFoldCase = false;
+ var ignoreCase = false;
+ for (var i = 0, n = regexs.length; i < n; ++i) {
+ var regex = regexs[i];
+ if (regex.ignoreCase) {
+ ignoreCase = true;
+ } else if (/[a-z]/i.test(regex.source.replace(
+ /\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi, ''))) {
+ needToFoldCase = true;
+ ignoreCase = false;
+ break;
+ }
+ }
+
+ var escapeCharToCodeUnit = {
+ 'b': 8,
+ 't': 9,
+ 'n': 0xa,
+ 'v': 0xb,
+ 'f': 0xc,
+ 'r': 0xd
+ };
+
+ function decodeEscape(charsetPart) {
+ var cc0 = charsetPart.charCodeAt(0);
+ if (cc0 !== 92 /* \\ */) {
+ return cc0;
+ }
+ var c1 = charsetPart.charAt(1);
+ cc0 = escapeCharToCodeUnit[c1];
+ if (cc0) {
+ return cc0;
+ } else if ('0' <= c1 && c1 <= '7') {
+ return parseInt(charsetPart.substring(1), 8);
+ } else if (c1 === 'u' || c1 === 'x') {
+ return parseInt(charsetPart.substring(2), 16);
+ } else {
+ return charsetPart.charCodeAt(1);
+ }
+ }
+
+ function encodeEscape(charCode) {
+ if (charCode < 0x20) {
+ return (charCode < 0x10 ? '\\x0' : '\\x') + charCode.toString(16);
+ }
+ var ch = String.fromCharCode(charCode);
+ if (ch === '\\' || ch === '-' || ch === '[' || ch === ']') {
+ ch = '\\' + ch;
+ }
+ return ch;
+ }
+
+ function caseFoldCharset(charSet) {
+ var charsetParts = charSet.substring(1, charSet.length - 1).match(
+ new RegExp(
+ '\\\\u[0-9A-Fa-f]{4}'
+ + '|\\\\x[0-9A-Fa-f]{2}'
+ + '|\\\\[0-3][0-7]{0,2}'
+ + '|\\\\[0-7]{1,2}'
+ + '|\\\\[\\s\\S]'
+ + '|-'
+ + '|[^-\\\\]',
+ 'g'));
+ var groups = [];
+ var ranges = [];
+ var inverse = charsetParts[0] === '^';
+ for (var i = inverse ? 1 : 0, n = charsetParts.length; i < n; ++i) {
+ var p = charsetParts[i];
+ if (/\\[bdsw]/i.test(p)) { // Don't muck with named groups.
+ groups.push(p);
+ } else {
+ var start = decodeEscape(p);
+ var end;
+ if (i + 2 < n && '-' === charsetParts[i + 1]) {
+ end = decodeEscape(charsetParts[i + 2]);
+ i += 2;
+ } else {
+ end = start;
+ }
+ ranges.push([start, end]);
+ // If the range might intersect letters, then expand it.
+ // This case handling is too simplistic.
+ // It does not deal with non-latin case folding.
+ // It works for latin source code identifiers though.
+ if (!(end < 65 || start > 122)) {
+ if (!(end < 65 || start > 90)) {
+ ranges.push([Math.max(65, start) | 32, Math.min(end, 90) | 32]);
+ }
+ if (!(end < 97 || start > 122)) {
+ ranges.push([Math.max(97, start) & ~32, Math.min(end, 122) & ~32]);
+ }
+ }
+ }
+ }
+
+ // [[1, 10], [3, 4], [8, 12], [14, 14], [16, 16], [17, 17]]
+ // -> [[1, 12], [14, 14], [16, 17]]
+ ranges.sort(function (a, b) { return (a[0] - b[0]) || (b[1] - a[1]); });
+ var consolidatedRanges = [];
+ var lastRange = [NaN, NaN];
+ for (var i = 0; i < ranges.length; ++i) {
+ var range = ranges[i];
+ if (range[0] <= lastRange[1] + 1) {
+ lastRange[1] = Math.max(lastRange[1], range[1]);
+ } else {
+ consolidatedRanges.push(lastRange = range);
+ }
+ }
+
+ var out = ['['];
+ if (inverse) { out.push('^'); }
+ out.push.apply(out, groups);
+ for (var i = 0; i < consolidatedRanges.length; ++i) {
+ var range = consolidatedRanges[i];
+ out.push(encodeEscape(range[0]));
+ if (range[1] > range[0]) {
+ if (range[1] + 1 > range[0]) { out.push('-'); }
+ out.push(encodeEscape(range[1]));
+ }
+ }
+ out.push(']');
+ return out.join('');
+ }
+
+ function allowAnywhereFoldCaseAndRenumberGroups(regex) {
+ // Split into character sets, escape sequences, punctuation strings
+ // like ('(', '(?:', ')', '^'), and runs of characters that do not
+ // include any of the above.
+ var parts = regex.source.match(
+ new RegExp(
+ '(?:'
+ + '\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]' // a character set
+ + '|\\\\u[A-Fa-f0-9]{4}' // a unicode escape
+ + '|\\\\x[A-Fa-f0-9]{2}' // a hex escape
+ + '|\\\\[0-9]+' // a back-reference or octal escape
+ + '|\\\\[^ux0-9]' // other escape sequence
+ + '|\\(\\?[:!=]' // start of a non-capturing group
+ + '|[\\(\\)\\^]' // start/emd of a group, or line start
+ + '|[^\\x5B\\x5C\\(\\)\\^]+' // run of other characters
+ + ')',
+ 'g'));
+ var n = parts.length;
+
+ // Maps captured group numbers to the number they will occupy in
+ // the output or to -1 if that has not been determined, or to
+ // undefined if they need not be capturing in the output.
+ var capturedGroups = [];
+
+ // Walk over and identify back references to build the capturedGroups
+ // mapping.
+ for (var i = 0, groupIndex = 0; i < n; ++i) {
+ var p = parts[i];
+ if (p === '(') {
+ // groups are 1-indexed, so max group index is count of '('
+ ++groupIndex;
+ } else if ('\\' === p.charAt(0)) {
+ var decimalValue = +p.substring(1);
+ if (decimalValue && decimalValue <= groupIndex) {
+ capturedGroups[decimalValue] = -1;
+ }
+ }
+ }
+
+ // Renumber groups and reduce capturing groups to non-capturing groups
+ // where possible.
+ for (var i = 1; i < capturedGroups.length; ++i) {
+ if (-1 === capturedGroups[i]) {
+ capturedGroups[i] = ++capturedGroupIndex;
+ }
+ }
+ for (var i = 0, groupIndex = 0; i < n; ++i) {
+ var p = parts[i];
+ if (p === '(') {
+ ++groupIndex;
+ if (capturedGroups[groupIndex] === undefined) {
+ parts[i] = '(?:';
+ }
+ } else if ('\\' === p.charAt(0)) {
+ var decimalValue = +p.substring(1);
+ if (decimalValue && decimalValue <= groupIndex) {
+ parts[i] = '\\' + capturedGroups[groupIndex];
+ }
+ }
+ }
+
+ // Remove any prefix anchors so that the output will match anywhere.
+ // ^^ really does mean an anchored match though.
+ for (var i = 0, groupIndex = 0; i < n; ++i) {
+ if ('^' === parts[i] && '^' !== parts[i + 1]) { parts[i] = ''; }
+ }
+
+ // Expand letters to groups to handle mixing of case-sensitive and
+ // case-insensitive patterns if necessary.
+ if (regex.ignoreCase && needToFoldCase) {
+ for (var i = 0; i < n; ++i) {
+ var p = parts[i];
+ var ch0 = p.charAt(0);
+ if (p.length >= 2 && ch0 === '[') {
+ parts[i] = caseFoldCharset(p);
+ } else if (ch0 !== '\\') {
+ // TODO: handle letters in numeric escapes.
+ parts[i] = p.replace(
+ /[a-zA-Z]/g,
+ function (ch) {
+ var cc = ch.charCodeAt(0);
+ return '[' + String.fromCharCode(cc & ~32, cc | 32) + ']';
+ });
+ }
+ }
+ }
+
+ return parts.join('');
+ }
+
+ var rewritten = [];
+ for (var i = 0, n = regexs.length; i < n; ++i) {
+ var regex = regexs[i];
+ if (regex.global || regex.multiline) { throw new Error('' + regex); }
+ rewritten.push(
+ '(?:' + allowAnywhereFoldCaseAndRenumberGroups(regex) + ')');
+ }
+
+ return new RegExp(rewritten.join('|'), ignoreCase ? 'gi' : 'g');
+ }
+
+
+ /**
+ * Split markup into a string of source code and an array mapping ranges in
+ * that string to the text nodes in which they appear.
+ *
+ *
+ * where #1 is a reference to the {@code "print "} text node above, and so
+ * on for the other text nodes.
+ *
+ *
+ *
+ * The {@code} spans array is an array of pairs. Even elements are the start
+ * indices of substrings, and odd elements are the text nodes (or BR elements)
+ * that contain the text for those substrings.
+ * Substrings continue until the next index or the end of the source.
+ *
+ *
+ * @param {Node} node an HTML DOM subtree containing source-code.
+ * @return {Object} source code and the text nodes in which they occur.
+ */
+ function extractSourceSpans(node) {
+ var nocode = /(?:^|\s)nocode(?:\s|$)/;
+
+ var chunks = [];
+ var length = 0;
+ var spans = [];
+ var k = 0;
+
+ var whitespace;
+ if (node.currentStyle) {
+ whitespace = node.currentStyle.whiteSpace;
+ } else if (window.getComputedStyle) {
+ whitespace = document.defaultView.getComputedStyle(node, null)
+ .getPropertyValue('white-space');
+ }
+ var isPreformatted = whitespace && 'pre' === whitespace.substring(0, 3);
+
+ function walk(node) {
+ switch (node.nodeType) {
+ case 1: // Element
+ if (nocode.test(node.className)) { return; }
+ for (var child = node.firstChild; child; child = child.nextSibling) {
+ walk(child);
+ }
+ var nodeName = node.nodeName;
+ if ('BR' === nodeName || 'LI' === nodeName) {
+ chunks[k] = '\n';
+ spans[k << 1] = length++;
+ spans[(k++ << 1) | 1] = node;
+ }
+ break;
+ case 3: case 4: // Text
+ var text = node.nodeValue;
+ if (text.length) {
+ if (!isPreformatted) {
+ text = text.replace(/[ \t\r\n]+/g, ' ');
+ } else {
+ text = text.replace(/\r\n?/g, '\n'); // Normalize newlines.
+ }
+ // TODO: handle tabs here?
+ chunks[k] = text;
+ spans[k << 1] = length;
+ length += text.length;
+ spans[(k++ << 1) | 1] = node;
+ }
+ break;
+ }
+ }
+
+ walk(node);
+
+ return {
+ sourceCode: chunks.join('').replace(/\n$/, ''),
+ spans: spans
+ };
+ }
+
+
+ /**
+ * Apply the given language handler to sourceCode and add the resulting
+ * decorations to out.
+ * @param {number} basePos the index of sourceCode within the chunk of source
+ * whose decorations are already present on out.
+ */
+ function appendDecorations(basePos, sourceCode, langHandler, out) {
+ if (!sourceCode) { return; }
+ var job = {
+ sourceCode: sourceCode,
+ basePos: basePos
+ };
+ langHandler(job);
+ out.push.apply(out, job.decorations);
+ }
+
+ var notWs = /\S/;
+
+ /**
+ * Given an element, if it contains only one child element and any text nodes
+ * it contains contain only space characters, return the sole child element.
+ * Otherwise returns undefined.
+ *
+ * This is meant to return the CODE element in {@code
} when
+ * there is a single child element that contains all the non-space textual
+ * content, but not to return anything where there are multiple child elements
+ * as in {@code
......
} or when there
+ * is textual content.
+ */
+ function childContentWrapper(element) {
+ var wrapper = undefined;
+ for (var c = element.firstChild; c; c = c.nextSibling) {
+ var type = c.nodeType;
+ wrapper = (type === 1) // Element Node
+ ? (wrapper ? element : c)
+ : (type === 3) // Text Node
+ ? (notWs.test(c.nodeValue) ? element : wrapper)
+ : wrapper;
+ }
+ return wrapper === element ? undefined : wrapper;
+ }
+
+ /** Given triples of [style, pattern, context] returns a lexing function,
+ * The lexing function interprets the patterns to find token boundaries and
+ * returns a decoration list of the form
+ * [index_0, style_0, index_1, style_1, ..., index_n, style_n]
+ * where index_n is an index into the sourceCode, and style_n is a style
+ * constant like PR_PLAIN. index_n-1 <= index_n, and style_n-1 applies to
+ * all characters in sourceCode[index_n-1:index_n].
+ *
+ * The stylePatterns is a list whose elements have the form
+ * [style : string, pattern : RegExp, DEPRECATED, shortcut : string].
+ *
+ * Style is a style constant like PR_PLAIN, or can be a string of the
+ * form 'lang-FOO', where FOO is a language extension describing the
+ * language of the portion of the token in $1 after pattern executes.
+ * E.g., if style is 'lang-lisp', and group 1 contains the text
+ * '(hello (world))', then that portion of the token will be passed to the
+ * registered lisp handler for formatting.
+ * The text before and after group 1 will be restyled using this decorator
+ * so decorators should take care that this doesn't result in infinite
+ * recursion. For example, the HTML lexer rule for SCRIPT elements looks
+ * something like ['lang-js', /<[s]cript>(.+?)<\/script>/]. This may match
+ * '
+
+
\ No newline at end of file
diff --git a/gears/harmonize/config/initialize.php b/gears/harmonize/config/initialize.php
new file mode 100644
index 0000000..25885fd
--- /dev/null
+++ b/gears/harmonize/config/initialize.php
@@ -0,0 +1,34 @@
+user())
+ {
+ $credentials = array(
+ 'id' => $user->id,
+ 'username' => $user->username,
+ 'email' => $user->email
+ );
+
+ // Attempt to authorize the user. Feather will deal with the response,
+ // all that needs to be done is the credentials passed in.
+ return $feather['sso']->authorize($credentials);
+ }
+}
\ No newline at end of file
diff --git a/gears/harmonize/gear.json b/gears/harmonize/gear.json
new file mode 100644
index 0000000..51a79f5
--- /dev/null
+++ b/gears/harmonize/gear.json
@@ -0,0 +1,12 @@
+{
+ "name":"Harmonize",
+ "description":"A Laravel application integration gear that allows Single Sign On.",
+ "version":"1.0.0",
+ "author":
+ {
+ "name":"Jason Lewis",
+ "email":"jason.lewis1991@gmail.com",
+ "website":"http://jasonlewis.me"
+ },
+ "dependencies":{}
+}
\ No newline at end of file
diff --git a/gears/harmonize/harmonize.gear.php b/gears/harmonize/harmonize.gear.php
new file mode 100644
index 0000000..7abbfe5
--- /dev/null
+++ b/gears/harmonize/harmonize.gear.php
@@ -0,0 +1,32 @@
+listen('auth: bootstrap harmonize', 'bootstrap');
+ }
+
+ /**
+ * Bootstrap the Harmonize plugin. Run the users initializer.
+ *
+ * @return mixed
+ */
+ public function bootstrap()
+ {
+ $initializer = $this->feather['config']->get('gear: harmonize initialize');
+
+ if($response = $initializer($this->feather))
+ {
+ return $response;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/gears/markdown/markdown.gear.php b/gears/markdown/markdown.gear.php
new file mode 100644
index 0000000..212feb7
--- /dev/null
+++ b/gears/markdown/markdown.gear.php
@@ -0,0 +1,2855 @@
+
+#
+# Original Markdown
+# Copyright (c) 2004-2006 John Gruber
+#
+#
+
+define( 'MARKDOWN_VERSION', "1.0.1o" ); # Sun 8 Jan 2012
+define( 'MARKDOWNEXTRA_VERSION', "1.2.5" ); # Sun 8 Jan 2012
+
+#
+# Global default settings:
+#
+
+# Change to ">" for HTML output
+@define( 'MARKDOWN_EMPTY_ELEMENT_SUFFIX', " />");
+
+# Define the width of a tab for code blocks.
+@define( 'MARKDOWN_TAB_WIDTH', 4 );
+
+# Optional title attribute for footnote links and backlinks.
+@define( 'MARKDOWN_FN_LINK_TITLE', "" );
+@define( 'MARKDOWN_FN_BACKLINK_TITLE', "" );
+
+# Optional class attribute for footnote links and backlinks.
+@define( 'MARKDOWN_FN_LINK_CLASS', "" );
+@define( 'MARKDOWN_FN_BACKLINK_CLASS', "" );
+
+
+### Standard Function Interface ###
+
+@define( 'MARKDOWN_PARSER_CLASS', 'Feather\\Gear\\Markdown\\MarkdownFeather_Parser' );
+
+function Parse($text) {
+#
+# Initialize the parser and return the result of its transform method.
+#
+ # Setup static parser variable.
+ static $parser;
+ if (!isset($parser)) {
+ $parser_class = MARKDOWN_PARSER_CLASS;
+ $parser = new $parser_class;
+ }
+
+ # Transform text using parser.
+ return $parser->transform($text);
+}
+
+#
+# Markdown Parser Class
+#
+
+class Markdown_Parser {
+
+ # Regex to match balanced [brackets].
+ # Needed to insert a maximum bracked depth while converting to PHP.
+ var $nested_brackets_depth = 6;
+ var $nested_brackets_re;
+
+ var $nested_url_parenthesis_depth = 4;
+ var $nested_url_parenthesis_re;
+
+ # Table of hash values for escaped characters:
+ var $escape_chars = '\`*_{}[]()>#+-.!';
+ var $escape_chars_re;
+
+ # Change to ">" for HTML output.
+ var $empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX;
+ var $tab_width = MARKDOWN_TAB_WIDTH;
+
+ # Change to `true` to disallow markup or entities.
+ var $no_markup = false;
+ var $no_entities = false;
+
+ # Predefined urls and titles for reference links and images.
+ var $predef_urls = array();
+ var $predef_titles = array();
+
+
+ function __construct() {
+ #
+ # Constructor function. Initialize appropriate member variables.
+ #
+ $this->_initDetab();
+ $this->prepareItalicsAndBold();
+
+ $this->nested_brackets_re =
+ str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth).
+ str_repeat('\])*', $this->nested_brackets_depth);
+
+ $this->nested_url_parenthesis_re =
+ str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth).
+ str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth);
+
+ $this->escape_chars_re = '['.preg_quote($this->escape_chars).']';
+
+ # Sort document, block, and span gamut in ascendent priority order.
+ asort($this->document_gamut);
+ asort($this->block_gamut);
+ asort($this->span_gamut);
+ }
+
+
+ # Internal hashes used during transformation.
+ var $urls = array();
+ var $titles = array();
+ var $html_hashes = array();
+
+ # Status flag to avoid invalid nesting.
+ var $in_anchor = false;
+
+
+ function setup() {
+ #
+ # Called before the transformation process starts to setup parser
+ # states.
+ #
+ # Clear global hashes.
+ $this->urls = $this->predef_urls;
+ $this->titles = $this->predef_titles;
+ $this->html_hashes = array();
+
+ $in_anchor = false;
+ }
+
+ function teardown() {
+ #
+ # Called after the transformation process to clear any variable
+ # which may be taking up memory unnecessarly.
+ #
+ $this->urls = array();
+ $this->titles = array();
+ $this->html_hashes = array();
+ }
+
+
+ function transform($text) {
+ #
+ # Main function. Performs some preprocessing on the input text
+ # and pass it through the document gamut.
+ #
+ $this->setup();
+
+ # Remove UTF-8 BOM and marker character in input, if present.
+ $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text);
+
+ # Standardize line endings:
+ # DOS to Unix and Mac to Unix
+ $text = preg_replace('{\r\n?}', "\n", $text);
+
+ # Make sure $text ends with a couple of newlines:
+ $text .= "\n\n";
+
+ # Convert all tabs to spaces.
+ $text = $this->detab($text);
+
+ # Turn block-level HTML blocks into hash entries
+ $text = $this->hashHTMLBlocks($text);
+
+ # Strip any lines consisting only of spaces and tabs.
+ # This makes subsequent regexen easier to write, because we can
+ # match consecutive blank lines with /\n+/ instead of something
+ # contorted like /[ ]*\n+/ .
+ $text = preg_replace('/^[ ]+$/m', '', $text);
+
+ # Run document gamut methods.
+ foreach ($this->document_gamut as $method => $priority) {
+ $text = $this->$method($text);
+ }
+
+ $this->teardown();
+
+ return $text . "\n";
+ }
+
+ var $document_gamut = array(
+ # Strip link definitions, store in hashes.
+ "stripLinkDefinitions" => 20,
+
+ "runBasicBlockGamut" => 30,
+ );
+
+
+ function stripLinkDefinitions($text) {
+ #
+ # Strips link definitions from text, stores the URLs and titles in
+ # hash references.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Link defs are in the form: ^[id]: url "optional title"
+ $text = preg_replace_callback('{
+ ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1
+ [ ]*
+ \n? # maybe *one* newline
+ [ ]*
+ (?:
+ <(.+?)> # url = $2
+ |
+ (\S+?) # url = $3
+ )
+ [ ]*
+ \n? # maybe one newline
+ [ ]*
+ (?:
+ (?<=\s) # lookbehind for whitespace
+ ["(]
+ (.*?) # title = $4
+ [")]
+ [ ]*
+ )? # title is optional
+ (?:\n+|\Z)
+ }xm',
+ array(&$this, '_stripLinkDefinitions_callback'),
+ $text);
+ return $text;
+ }
+ function _stripLinkDefinitions_callback($matches) {
+ $link_id = strtolower($matches[1]);
+ $url = $matches[2] == '' ? $matches[3] : $matches[2];
+ $this->urls[$link_id] = $url;
+ $this->titles[$link_id] =& $matches[4];
+ return ''; # String that will replace the block
+ }
+
+
+ function hashHTMLBlocks($text) {
+ if ($this->no_markup) return $text;
+
+ $less_than_tab = $this->tab_width - 1;
+
+ # Hashify HTML blocks:
+ # We only want to do this for block-level HTML tags, such as headers,
+ # lists, and tables. That's because we still want to wrap
s around
+ # "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+ # phrase emphasis, and spans. The list of tags we're looking for is
+ # hard-coded:
+ #
+ # * List "a" is made of tags which can be both inline or block-level.
+ # These will be treated block-level when the start tag is alone on
+ # its line, otherwise they're not matched here and will be taken as
+ # inline later.
+ # * List "b" is made of tags which are always block-level;
+ #
+ $block_tags_a_re = 'ins|del';
+ $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'.
+ 'script|noscript|form|fieldset|iframe|math';
+
+ # Regular expression for the content of a block tag.
+ $nested_tags_level = 4;
+ $attr = '
+ (?> # optional tag attributes
+ \s # starts with whitespace
+ (?>
+ [^>"/]+ # text outside quotes
+ |
+ /+(?!>) # slash not followed by ">"
+ |
+ "[^"]*" # text inside double quotes (tolerate ">")
+ |
+ \'[^\']*\' # text inside single quotes (tolerate ">")
+ )*
+ )?
+ ';
+ $content =
+ str_repeat('
+ (?>
+ [^<]+ # content without tag
+ |
+ <\2 # nested opening tag
+ '.$attr.' # attributes
+ (?>
+ />
+ |
+ >', $nested_tags_level). # end of opening tag
+ '.*?'. # last level nested tag content
+ str_repeat('
+ \2\s*> # closing nested tag
+ )
+ |
+ <(?!/\2\s*> # other tags with a different name
+ )
+ )*',
+ $nested_tags_level);
+ $content2 = str_replace('\2', '\3', $content);
+
+ # First, look for nested blocks, e.g.:
+ #
+ #
+ # tags for inner block must be indented.
+ #
+ #
+ #
+ # The outermost tags must start at the left margin for this to match, and
+ # the inner nested divs must be indented.
+ # We need to do this before the next, more liberal match, because the next
+ # match will start at the first `
` and stop at the first `
`.
+ $text = preg_replace_callback('{(?>
+ (?>
+ (?<=\n\n) # Starting after a blank line
+ | # or
+ \A\n? # the beginning of the doc
+ )
+ ( # save in $1
+
+ # Match from `\n` to `\n`, handling nested tags
+ # in between.
+
+ [ ]{0,'.$less_than_tab.'}
+ <('.$block_tags_b_re.')# start tag = $2
+ '.$attr.'> # attributes followed by > and \n
+ '.$content.' # content, support nesting
+ \2> # the matching end tag
+ [ ]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+
+ | # Special version for tags of group a.
+
+ [ ]{0,'.$less_than_tab.'}
+ <('.$block_tags_a_re.')# start tag = $3
+ '.$attr.'>[ ]*\n # attributes followed by >
+ '.$content2.' # content, support nesting
+ \3> # the matching end tag
+ [ ]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+
+ | # Special case just for . It was easier to make a special
+ # case than to make the other regex more complicated.
+
+ [ ]{0,'.$less_than_tab.'}
+ <(hr) # start tag = $2
+ '.$attr.' # attributes
+ /?> # the matching end tag
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ | # Special case for standalone HTML comments:
+
+ [ ]{0,'.$less_than_tab.'}
+ (?s:
+
+ )
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ | # PHP and ASP-style processor instructions ( and <%)
+
+ [ ]{0,'.$less_than_tab.'}
+ (?s:
+ <([?%]) # $2
+ .*?
+ \2>
+ )
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ )
+ )}Sxmi',
+ array(&$this, '_hashHTMLBlocks_callback'),
+ $text);
+
+ return $text;
+ }
+ function _hashHTMLBlocks_callback($matches) {
+ $text = $matches[1];
+ $key = $this->hashBlock($text);
+ return "\n\n$key\n\n";
+ }
+
+
+ function hashPart($text, $boundary = 'X') {
+ #
+ # Called whenever a tag must be hashed when a function insert an atomic
+ # element in the text stream. Passing $text to through this function gives
+ # a unique text-token which will be reverted back when calling unhash.
+ #
+ # The $boundary argument specify what character should be used to surround
+ # the token. By convension, "B" is used for block elements that needs not
+ # to be wrapped into paragraph tags at the end, ":" is used for elements
+ # that are word separators and "X" is used in the general case.
+ #
+ # Swap back any tag hash found in $text so we do not have to `unhash`
+ # multiple times at the end.
+ $text = $this->unhash($text);
+
+ # Then hash the block.
+ static $i = 0;
+ $key = "$boundary\x1A" . ++$i . $boundary;
+ $this->html_hashes[$key] = $text;
+ return $key; # String that will replace the tag.
+ }
+
+
+ function hashBlock($text) {
+ #
+ # Shortcut function for hashPart with block-level boundaries.
+ #
+ return $this->hashPart($text, 'B');
+ }
+
+
+ var $block_gamut = array(
+ #
+ # These are all the transformations that form block-level
+ # tags like paragraphs, headers, and list items.
+ #
+ "doHeaders" => 10,
+ "doHorizontalRules" => 20,
+
+ "doLists" => 40,
+ "doCodeBlocks" => 50,
+ "doBlockQuotes" => 60,
+ );
+
+ function runBlockGamut($text) {
+ #
+ # Run block gamut tranformations.
+ #
+ # We need to escape raw HTML in Markdown source before doing anything
+ # else. This need to be done for each block, and not only at the
+ # begining in the Markdown function since hashed blocks can be part of
+ # list items and could have been indented. Indented blocks would have
+ # been seen as a code block in a previous pass of hashHTMLBlocks.
+ $text = $this->hashHTMLBlocks($text);
+
+ return $this->runBasicBlockGamut($text);
+ }
+
+ function runBasicBlockGamut($text) {
+ #
+ # Run block gamut tranformations, without hashing HTML blocks. This is
+ # useful when HTML blocks are known to be already hashed, like in the first
+ # whole-document pass.
+ #
+ foreach ($this->block_gamut as $method => $priority) {
+ $text = $this->$method($text);
+ }
+
+ # Finally form paragraph and restore hashed blocks.
+ $text = $this->formParagraphs($text);
+
+ return $text;
+ }
+
+
+ function doHorizontalRules($text) {
+ # Do Horizontal Rules:
+ return preg_replace(
+ '{
+ ^[ ]{0,3} # Leading space
+ ([-*_]) # $1: First marker
+ (?> # Repeated marker group
+ [ ]{0,2} # Zero, one, or two spaces.
+ \1 # Marker character
+ ){2,} # Group repeated at least twice
+ [ ]* # Tailing spaces
+ $ # End of line.
+ }mx',
+ "\n".$this->hashBlock("empty_element_suffix")."\n",
+ $text);
+ }
+
+
+ var $span_gamut = array(
+ #
+ # These are all the transformations that occur *within* block-level
+ # tags like paragraphs, headers, and list items.
+ #
+ # Process character escapes, code spans, and inline HTML
+ # in one shot.
+ "parseSpan" => -30,
+
+ # Process anchor and image tags. Images must come first,
+ # because ![foo][f] looks like an anchor.
+ "doImages" => 10,
+ "doAnchors" => 20,
+
+ # Make links out of things like ``
+ # Must come after doAnchors, because you can use < and >
+ # delimiters in inline links like [this]().
+ "doAutoLinks" => 30,
+ "encodeAmpsAndAngles" => 40,
+
+ "doItalicsAndBold" => 50,
+ "doHardBreaks" => 60,
+ );
+
+ function runSpanGamut($text) {
+ #
+ # Run span gamut tranformations.
+ #
+ foreach ($this->span_gamut as $method => $priority) {
+ $text = $this->$method($text);
+ }
+
+ return $text;
+ }
+
+
+ function doHardBreaks($text) {
+ # Do hard breaks:
+ return preg_replace_callback('/ {2,}\n/',
+ array(&$this, '_doHardBreaks_callback'), $text);
+ }
+ function _doHardBreaks_callback($matches) {
+ return $this->hashPart(" empty_element_suffix\n");
+ }
+
+
+ function doAnchors($text) {
+ #
+ # Turn Markdown link shortcuts into XHTML tags.
+ #
+ if ($this->in_anchor) return $text;
+ $this->in_anchor = true;
+
+ #
+ # First, handle reference-style links: [link text] [id]
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ('.$this->nested_brackets_re.') # link text = $2
+ \]
+
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+
+ \[
+ (.*?) # id = $3
+ \]
+ )
+ }xs',
+ array(&$this, '_doAnchors_reference_callback'), $text);
+
+ #
+ # Next, inline-style links: [link text](url "optional title")
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ('.$this->nested_brackets_re.') # link text = $2
+ \]
+ \( # literal paren
+ [ \n]*
+ (?:
+ <(.+?)> # href = $3
+ |
+ ('.$this->nested_url_parenthesis_re.') # href = $4
+ )
+ [ \n]*
+ ( # $5
+ ([\'"]) # quote char = $6
+ (.*?) # Title = $7
+ \6 # matching quote
+ [ \n]* # ignore any spaces/tabs between closing quote and )
+ )? # title is optional
+ \)
+ )
+ }xs',
+ array(&$this, '_doAnchors_inline_callback'), $text);
+
+ #
+ # Last, handle reference-style shortcuts: [link text]
+ # These must come last in case you've also got [link text][1]
+ # or [link text](/foo)
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ([^\[\]]+) # link text = $2; can\'t contain [ or ]
+ \]
+ )
+ }xs',
+ array(&$this, '_doAnchors_reference_callback'), $text);
+
+ $this->in_anchor = false;
+ return $text;
+ }
+ function _doAnchors_reference_callback($matches) {
+ $whole_match = $matches[1];
+ $link_text = $matches[2];
+ $link_id =& $matches[3];
+
+ if ($link_id == "") {
+ # for shortcut links like [this][] or [this].
+ $link_id = $link_text;
+ }
+
+ # lower-case and turn embedded newlines into spaces
+ $link_id = strtolower($link_id);
+ $link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
+
+ if (isset($this->urls[$link_id])) {
+ $url = $this->urls[$link_id];
+ $url = $this->encodeAttribute($url);
+
+ $result = "titles[$link_id] ) ) {
+ $title = $this->titles[$link_id];
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+
+ $link_text = $this->runSpanGamut($link_text);
+ $result .= ">$link_text";
+ $result = $this->hashPart($result);
+ }
+ else {
+ $result = $whole_match;
+ }
+ return $result;
+ }
+ function _doAnchors_inline_callback($matches) {
+ $whole_match = $matches[1];
+ $link_text = $this->runSpanGamut($matches[2]);
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
+ $title =& $matches[7];
+
+ $url = $this->encodeAttribute($url);
+
+ $result = "encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+
+ $link_text = $this->runSpanGamut($link_text);
+ $result .= ">$link_text";
+
+ return $this->hashPart($result);
+ }
+
+
+ function doImages($text) {
+ #
+ # Turn Markdown image shortcuts into tags.
+ #
+ #
+ # First, handle reference-style labeled images: ![alt text][id]
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ !\[
+ ('.$this->nested_brackets_re.') # alt text = $2
+ \]
+
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+
+ \[
+ (.*?) # id = $3
+ \]
+
+ )
+ }xs',
+ array(&$this, '_doImages_reference_callback'), $text);
+
+ #
+ # Next, handle inline images: 
+ # Don't forget: encode * and _
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ !\[
+ ('.$this->nested_brackets_re.') # alt text = $2
+ \]
+ \s? # One optional whitespace character
+ \( # literal paren
+ [ \n]*
+ (?:
+ <(\S*)> # src url = $3
+ |
+ ('.$this->nested_url_parenthesis_re.') # src url = $4
+ )
+ [ \n]*
+ ( # $5
+ ([\'"]) # quote char = $6
+ (.*?) # title = $7
+ \6 # matching quote
+ [ \n]*
+ )? # title is optional
+ \)
+ )
+ }xs',
+ array(&$this, '_doImages_inline_callback'), $text);
+
+ return $text;
+ }
+ function _doImages_reference_callback($matches) {
+ $whole_match = $matches[1];
+ $alt_text = $matches[2];
+ $link_id = strtolower($matches[3]);
+
+ if ($link_id == "") {
+ $link_id = strtolower($alt_text); # for shortcut links like ![this][].
+ }
+
+ $alt_text = $this->encodeAttribute($alt_text);
+ if (isset($this->urls[$link_id])) {
+ $url = $this->encodeAttribute($this->urls[$link_id]);
+ $result = "titles[$link_id])) {
+ $title = $this->titles[$link_id];
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+ $result .= $this->empty_element_suffix;
+ $result = $this->hashPart($result);
+ }
+ else {
+ # If there's no such link ID, leave intact:
+ $result = $whole_match;
+ }
+
+ return $result;
+ }
+ function _doImages_inline_callback($matches) {
+ $whole_match = $matches[1];
+ $alt_text = $matches[2];
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
+ $title =& $matches[7];
+
+ $alt_text = $this->encodeAttribute($alt_text);
+ $url = $this->encodeAttribute($url);
+ $result = "encodeAttribute($title);
+ $result .= " title=\"$title\""; # $title already quoted
+ }
+ $result .= $this->empty_element_suffix;
+
+ return $this->hashPart($result);
+ }
+
+
+ function doHeaders($text) {
+ # Setext-style headers:
+ # Header 1
+ # ========
+ #
+ # Header 2
+ # --------
+ #
+ $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx',
+ array(&$this, '_doHeaders_callback_setext'), $text);
+
+ # atx-style headers:
+ # # Header 1
+ # ## Header 2
+ # ## Header 2 with closing hashes ##
+ # ...
+ # ###### Header 6
+ #
+ $text = preg_replace_callback('{
+ ^(\#{1,6}) # $1 = string of #\'s
+ [ ]*
+ (.+?) # $2 = Header text
+ [ ]*
+ \#* # optional closing #\'s (not counted)
+ \n+
+ }xm',
+ array(&$this, '_doHeaders_callback_atx'), $text);
+
+ return $text;
+ }
+ function _doHeaders_callback_setext($matches) {
+ # Terrible hack to check we haven't found an empty list item.
+ if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1]))
+ return $matches[0];
+
+ $level = $matches[2]{0} == '=' ? 1 : 2;
+ $block = "".$this->runSpanGamut($matches[1])."";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+ function _doHeaders_callback_atx($matches) {
+ $level = strlen($matches[1]);
+ $block = "".$this->runSpanGamut($matches[2])."";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+
+
+ function doLists($text) {
+ #
+ # Form HTML ordered (numbered) and unordered (bulleted) lists.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Re-usable patterns to match list item bullets and number markers:
+ $marker_ul_re = '[*+-]';
+ $marker_ol_re = '\d+[\.]';
+ $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
+
+ $markers_relist = array(
+ $marker_ul_re => $marker_ol_re,
+ $marker_ol_re => $marker_ul_re,
+ );
+
+ foreach ($markers_relist as $marker_re => $other_marker_re) {
+ # Re-usable pattern to match any entirel ul or ol list:
+ $whole_list_re = '
+ ( # $1 = whole list
+ ( # $2
+ ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces
+ ('.$marker_re.') # $4 = first list item marker
+ [ ]+
+ )
+ (?s:.+?)
+ ( # $5
+ \z
+ |
+ \n{2,}
+ (?=\S)
+ (?! # Negative lookahead for another list item marker
+ [ ]*
+ '.$marker_re.'[ ]+
+ )
+ |
+ (?= # Lookahead for another kind of list
+ \n
+ \3 # Must have the same indentation
+ '.$other_marker_re.'[ ]+
+ )
+ )
+ )
+ '; // mx
+
+ # We use a different prefix before nested lists than top-level lists.
+ # See extended comment in _ProcessListItems().
+
+ if ($this->list_level) {
+ $text = preg_replace_callback('{
+ ^
+ '.$whole_list_re.'
+ }mx',
+ array(&$this, '_doLists_callback'), $text);
+ }
+ else {
+ $text = preg_replace_callback('{
+ (?:(?<=\n)\n|\A\n?) # Must eat the newline
+ '.$whole_list_re.'
+ }mx',
+ array(&$this, '_doLists_callback'), $text);
+ }
+ }
+
+ return $text;
+ }
+ function _doLists_callback($matches) {
+ # Re-usable patterns to match list item bullets and number markers:
+ $marker_ul_re = '[*+-]';
+ $marker_ol_re = '\d+[\.]';
+ $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
+
+ $list = $matches[1];
+ $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol";
+
+ $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re );
+
+ $list .= "\n";
+ $result = $this->processListItems($list, $marker_any_re);
+
+ $result = $this->hashBlock("<$list_type>\n" . $result . "$list_type>");
+ return "\n". $result ."\n\n";
+ }
+
+ var $list_level = 0;
+
+ function processListItems($list_str, $marker_any_re) {
+ #
+ # Process the contents of a single ordered or unordered list, splitting it
+ # into individual list items.
+ #
+ # The $this->list_level global keeps track of when we're inside a list.
+ # Each time we enter a list, we increment it; when we leave a list,
+ # we decrement. If it's zero, we're not in a list anymore.
+ #
+ # We do this because when we're not inside a list, we want to treat
+ # something like this:
+ #
+ # I recommend upgrading to version
+ # 8. Oops, now this line is treated
+ # as a sub-list.
+ #
+ # As a single paragraph, despite the fact that the second line starts
+ # with a digit-period-space sequence.
+ #
+ # Whereas when we're inside a list (or sub-list), that line will be
+ # treated as the start of a sub-list. What a kludge, huh? This is
+ # an aspect of Markdown's syntax that's hard to parse perfectly
+ # without resorting to mind-reading. Perhaps the solution is to
+ # change the syntax rules such that sub-lists must start with a
+ # starting cardinal number; e.g. "1." or "a.".
+
+ $this->list_level++;
+
+ # trim trailing blank lines:
+ $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
+
+ $list_str = preg_replace_callback('{
+ (\n)? # leading line = $1
+ (^[ ]*) # leading whitespace = $2
+ ('.$marker_any_re.' # list marker and space = $3
+ (?:[ ]+|(?=\n)) # space only required if item is not empty
+ )
+ ((?s:.*?)) # list item text = $4
+ (?:(\n+(?=\n))|\n) # tailing blank line = $5
+ (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n))))
+ }xm',
+ array(&$this, '_processListItems_callback'), $list_str);
+
+ $this->list_level--;
+ return $list_str;
+ }
+ function _processListItems_callback($matches) {
+ $item = $matches[4];
+ $leading_line =& $matches[1];
+ $leading_space =& $matches[2];
+ $marker_space = $matches[3];
+ $tailing_blank_line =& $matches[5];
+
+ if ($leading_line || $tailing_blank_line ||
+ preg_match('/\n{2,}/', $item))
+ {
+ # Replace marker with the appropriate whitespace indentation
+ $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item;
+ $item = $this->runBlockGamut($this->outdent($item)."\n");
+ }
+ else {
+ # Recursion for sub-lists:
+ $item = $this->doLists($this->outdent($item));
+ $item = preg_replace('/\n+$/', '', $item);
+ $item = $this->runSpanGamut($item);
+ }
+
+ return "
" . $item . "
\n";
+ }
+
+
+ function doCodeBlocks($text) {
+ #
+ # Process Markdown `
` blocks.
+ #
+ $text = preg_replace_callback('{
+ (?:\n\n|\A\n?)
+ ( # $1 = the code block -- one or more lines, starting with a space/tab
+ (?>
+ [ ]{'.$this->tab_width.'} # Lines must start with a tab or a tab-width of spaces
+ .*\n+
+ )+
+ )
+ ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
+ }xm',
+ array(&$this, '_doCodeBlocks_callback'), $text);
+
+ return $text;
+ }
+ function _doCodeBlocks_callback($matches) {
+ $codeblock = $matches[1];
+
+ $codeblock = $this->outdent($codeblock);
+ $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
+
+ # trim leading newlines and trailing newlines
+ $codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
+
+ $codeblock = "
$codeblock\n
";
+ return "\n\n".$this->hashBlock($codeblock)."\n\n";
+ }
+
+
+ function makeCodeSpan($code) {
+ #
+ # Create a code span markup for $code. Called from handleSpanToken.
+ #
+ $code = htmlspecialchars(trim($code), ENT_NOQUOTES);
+ return $this->hashPart("$code");
+ }
+
+
+ var $em_relist = array(
+ '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(?em_relist as $em => $em_re) {
+ foreach ($this->strong_relist as $strong => $strong_re) {
+ # Construct list of allowed token expressions.
+ $token_relist = array();
+ if (isset($this->em_strong_relist["$em$strong"])) {
+ $token_relist[] = $this->em_strong_relist["$em$strong"];
+ }
+ $token_relist[] = $em_re;
+ $token_relist[] = $strong_re;
+
+ # Construct master expression from list.
+ $token_re = '{('. implode('|', $token_relist) .')}';
+ $this->em_strong_prepared_relist["$em$strong"] = $token_re;
+ }
+ }
+ }
+
+ function doItalicsAndBold($text) {
+ $token_stack = array('');
+ $text_stack = array('');
+ $em = '';
+ $strong = '';
+ $tree_char_em = false;
+
+ while (1) {
+ #
+ # Get prepared regular expression for seraching emphasis tokens
+ # in current context.
+ #
+ $token_re = $this->em_strong_prepared_relist["$em$strong"];
+
+ #
+ # Each loop iteration search for the next emphasis token.
+ # Each token is then passed to handleSpanToken.
+ #
+ $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
+ $text_stack[0] .= $parts[0];
+ $token =& $parts[1];
+ $text =& $parts[2];
+
+ if (empty($token)) {
+ # Reached end of text span: empty stack without emitting.
+ # any more emphasis.
+ while ($token_stack[0]) {
+ $text_stack[1] .= array_shift($token_stack);
+ $text_stack[0] .= array_shift($text_stack);
+ }
+ break;
+ }
+
+ $token_len = strlen($token);
+ if ($tree_char_em) {
+ # Reached closing marker while inside a three-char emphasis.
+ if ($token_len == 3) {
+ # Three-char closing marker, close em and strong.
+ array_shift($token_stack);
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "$span";
+ $text_stack[0] .= $this->hashPart($span);
+ $em = '';
+ $strong = '';
+ } else {
+ # Other closing marker: close one em or strong and
+ # change current token state to match the other
+ $token_stack[0] = str_repeat($token{0}, 3-$token_len);
+ $tag = $token_len == 2 ? "strong" : "em";
+ $span = $text_stack[0];
+ $span = $this->runSpanGamut($span);
+ $span = "<$tag>$span$tag>";
+ $text_stack[0] = $this->hashPart($span);
+ $$tag = ''; # $$tag stands for $em or $strong
+ }
+ $tree_char_em = false;
+ } else if ($token_len == 3) {
+ if ($em) {
+ # Reached closing marker for both em and strong.
+ # Closing strong marker:
+ for ($i = 0; $i < 2; ++$i) {
+ $shifted_token = array_shift($token_stack);
+ $tag = strlen($shifted_token) == 2 ? "strong" : "em";
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "<$tag>$span$tag>";
+ $text_stack[0] .= $this->hashPart($span);
+ $$tag = ''; # $$tag stands for $em or $strong
+ }
+ } else {
+ # Reached opening three-char emphasis marker. Push on token
+ # stack; will be handled by the special condition above.
+ $em = $token{0};
+ $strong = "$em$em";
+ array_unshift($token_stack, $token);
+ array_unshift($text_stack, '');
+ $tree_char_em = true;
+ }
+ } else if ($token_len == 2) {
+ if ($strong) {
+ # Unwind any dangling emphasis marker:
+ if (strlen($token_stack[0]) == 1) {
+ $text_stack[1] .= array_shift($token_stack);
+ $text_stack[0] .= array_shift($text_stack);
+ }
+ # Closing strong marker:
+ array_shift($token_stack);
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "$span";
+ $text_stack[0] .= $this->hashPart($span);
+ $strong = '';
+ } else {
+ array_unshift($token_stack, $token);
+ array_unshift($text_stack, '');
+ $strong = $token;
+ }
+ } else {
+ # Here $token_len == 1
+ if ($em) {
+ if (strlen($token_stack[0]) == 1) {
+ # Closing emphasis marker:
+ array_shift($token_stack);
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "$span";
+ $text_stack[0] .= $this->hashPart($span);
+ $em = '';
+ } else {
+ $text_stack[0] .= $token;
+ }
+ } else {
+ array_unshift($token_stack, $token);
+ array_unshift($text_stack, '');
+ $em = $token;
+ }
+ }
+ }
+ return $text_stack[0];
+ }
+
+
+ function doBlockQuotes($text) {
+ $text = preg_replace_callback('/
+ ( # Wrap whole match in $1
+ (?>
+ ^[ ]*>[ ]? # ">" at the start of a line
+ .+\n # rest of the first line
+ (.+\n)* # subsequent consecutive lines
+ \n* # blanks
+ )+
+ )
+ /xm',
+ array(&$this, '_doBlockQuotes_callback'), $text);
+
+ return $text;
+ }
+ function _doBlockQuotes_callback($matches) {
+ $bq = $matches[1];
+ # trim one level of quoting - trim whitespace-only lines
+ $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq);
+ $bq = $this->runBlockGamut($bq); # recurse
+
+ $bq = preg_replace('/^/m', " ", $bq);
+ # These leading spaces cause problem with
content,
+ # so we need to fix that:
+ $bq = preg_replace_callback('{(\s*
+ #
+ # Based by a filter by Matthew Wickline, posted to BBEdit-Talk.
+ # With some optimizations by Milian Wolff.
+ #
+ $addr = "mailto:" . $addr;
+ $chars = preg_split('/(? $char) {
+ $ord = ord($char);
+ # Ignore non-ascii chars.
+ if ($ord < 128) {
+ $r = ($seed * (1 + $key)) % 100; # Pseudo-random function.
+ # roughly 10% raw, 45% hex, 45% dec
+ # '@' *must* be encoded. I insist.
+ if ($r > 90 && $char != '@') /* do nothing */;
+ else if ($r < 45) $chars[$key] = ''.dechex($ord).';';
+ else $chars[$key] = ''.$ord.';';
+ }
+ }
+
+ $addr = implode('', $chars);
+ $text = implode('', array_slice($chars, 7)); # text without `mailto:`
+ $addr = "$text";
+
+ return $addr;
+ }
+
+
+ function parseSpan($str) {
+ #
+ # Take the string $str and parse it into tokens, hashing embeded HTML,
+ # escaped characters and handling code spans.
+ #
+ $output = '';
+
+ $span_re = '{
+ (
+ \\\\'.$this->escape_chars_re.'
+ |
+ (?no_markup ? '' : '
+ |
+ # comment
+ |
+ <\?.*?\?> | <%.*?%> # processing instruction
+ |
+ <[/!$]?[-a-zA-Z0-9:_]+ # regular tags
+ (?>
+ \s
+ (?>[^"\'>]+|"[^"]*"|\'[^\']*\')*
+ )?
+ >
+ ').'
+ )
+ }xs';
+
+ while (1) {
+ #
+ # Each loop iteration seach for either the next tag, the next
+ # openning code span marker, or the next escaped character.
+ # Each token is then passed to handleSpanToken.
+ #
+ $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE);
+
+ # Create token from text preceding tag.
+ if ($parts[0] != "") {
+ $output .= $parts[0];
+ }
+
+ # Check if we reach the end.
+ if (isset($parts[1])) {
+ $output .= $this->handleSpanToken($parts[1], $parts[2]);
+ $str = $parts[2];
+ }
+ else {
+ break;
+ }
+ }
+
+ return $output;
+ }
+
+
+ function handleSpanToken($token, &$str) {
+ #
+ # Handle $token provided by parseSpan by determining its nature and
+ # returning the corresponding value that should replace it.
+ #
+ switch ($token{0}) {
+ case "\\":
+ return $this->hashPart("". ord($token{1}). ";");
+ case "`":
+ # Search for end marker in remaining text.
+ if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm',
+ $str, $matches))
+ {
+ $str = $matches[2];
+ $codespan = $this->makeCodeSpan($matches[1]);
+ return $this->hashPart($codespan);
+ }
+ return $token; // return as text since no ending marker found.
+ default:
+ return $this->hashPart($token);
+ }
+ }
+
+
+ function outdent($text) {
+ #
+ # Remove one level of line-leading tabs or spaces
+ #
+ return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text);
+ }
+
+
+ # String length function for detab. `_initDetab` will create a function to
+ # hanlde UTF-8 if the default function does not exist.
+ var $utf8_strlen = 'mb_strlen';
+
+ function detab($text) {
+ #
+ # Replace tabs with the appropriate amount of space.
+ #
+ # For each line we separate the line in blocks delemited by
+ # tab characters. Then we reconstruct every line by adding the
+ # appropriate number of space between each blocks.
+
+ $text = preg_replace_callback('/^.*\t.*$/m',
+ array(&$this, '_detab_callback'), $text);
+
+ return $text;
+ }
+ function _detab_callback($matches) {
+ $line = $matches[0];
+ $strlen = $this->utf8_strlen; # strlen function for UTF-8.
+
+ # Split in blocks.
+ $blocks = explode("\t", $line);
+ # Add each blocks to the line.
+ $line = $blocks[0];
+ unset($blocks[0]); # Do not add first block twice.
+ foreach ($blocks as $block) {
+ # Calculate amount of space, insert spaces, insert block.
+ $amount = $this->tab_width -
+ $strlen($line, 'UTF-8') % $this->tab_width;
+ $line .= str_repeat(" ", $amount) . $block;
+ }
+ return $line;
+ }
+ function _initDetab() {
+ #
+ # Check for the availability of the function in the `utf8_strlen` property
+ # (initially `mb_strlen`). If the function is not available, create a
+ # function that will loosely count the number of UTF-8 characters with a
+ # regular expression.
+ #
+ if (function_exists($this->utf8_strlen)) return;
+ $this->utf8_strlen = create_function('$text', 'return preg_match_all(
+ "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/",
+ $text, $m);');
+ }
+
+
+ function unhash($text) {
+ #
+ # Swap back in all the tags hashed by _HashHTMLBlocks.
+ #
+ return preg_replace_callback('/(.)\x1A[0-9]+\1/',
+ array(&$this, '_unhash_callback'), $text);
+ }
+ function _unhash_callback($matches) {
+ return $this->html_hashes[$matches[0]];
+ }
+
+}
+
+
+#
+# Markdown Extra Parser Class
+#
+
+class MarkdownExtra_Parser extends Markdown_Parser {
+
+ # Prefix for footnote ids.
+ var $fn_id_prefix = "";
+
+ # Optional title attribute for footnote links and backlinks.
+ var $fn_link_title = MARKDOWN_FN_LINK_TITLE;
+ var $fn_backlink_title = MARKDOWN_FN_BACKLINK_TITLE;
+
+ # Optional class attribute for footnote links and backlinks.
+ var $fn_link_class = MARKDOWN_FN_LINK_CLASS;
+ var $fn_backlink_class = MARKDOWN_FN_BACKLINK_CLASS;
+
+ # Predefined abbreviations.
+ var $predef_abbr = array();
+
+
+ function __construct() {
+ #
+ # Constructor function. Initialize the parser object.
+ #
+ # Add extra escapable characters before parent constructor
+ # initialize the table.
+ $this->escape_chars .= ':|';
+
+ # Insert extra document, block, and span transformations.
+ # Parent constructor will do the sorting.
+ $this->document_gamut += array(
+ "doFencedCodeBlocks" => 5,
+ "stripFootnotes" => 15,
+ "stripAbbreviations" => 25,
+ "appendFootnotes" => 50,
+ );
+ $this->block_gamut += array(
+ "doFencedCodeBlocks" => 5,
+ "doTables" => 15,
+ "doDefLists" => 45,
+ );
+ $this->span_gamut += array(
+ "doFootnotes" => 5,
+ "doAbbreviations" => 70,
+ );
+
+ parent::__construct();
+ }
+
+
+ # Extra variables used during extra transformations.
+ var $footnotes = array();
+ var $footnotes_ordered = array();
+ var $abbr_desciptions = array();
+ var $abbr_word_re = '';
+
+ # Give the current footnote number.
+ var $footnote_counter = 1;
+
+
+ function setup() {
+ #
+ # Setting up Extra-specific variables.
+ #
+ parent::setup();
+
+ $this->footnotes = array();
+ $this->footnotes_ordered = array();
+ $this->abbr_desciptions = array();
+ $this->abbr_word_re = '';
+ $this->footnote_counter = 1;
+
+ foreach ($this->predef_abbr as $abbr_word => $abbr_desc) {
+ if ($this->abbr_word_re)
+ $this->abbr_word_re .= '|';
+ $this->abbr_word_re .= preg_quote($abbr_word);
+ $this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
+ }
+ }
+
+ function teardown() {
+ #
+ # Clearing Extra-specific variables.
+ #
+ $this->footnotes = array();
+ $this->footnotes_ordered = array();
+ $this->abbr_desciptions = array();
+ $this->abbr_word_re = '';
+
+ parent::teardown();
+ }
+
+
+ ### HTML Block Parser ###
+
+ # Tags that are always treated as block tags:
+ var $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend';
+
+ # Tags treated as block tags only if the opening tag is alone on it's line:
+ var $context_block_tags_re = 'script|noscript|math|ins|del';
+
+ # Tags where markdown="1" default to span mode:
+ var $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address';
+
+ # Tags which must not have their contents modified, no matter where
+ # they appear:
+ var $clean_tags_re = 'script|math';
+
+ # Tags that do not need to be closed.
+ var $auto_close_tags_re = 'hr|img';
+
+
+ function hashHTMLBlocks($text) {
+ #
+ # Hashify HTML Blocks and "clean tags".
+ #
+ # We only want to do this for block-level HTML tags, such as headers,
+ # lists, and tables. That's because we still want to wrap
s around
+ # "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+ # phrase emphasis, and spans. The list of tags we're looking for is
+ # hard-coded.
+ #
+ # This works by calling _HashHTMLBlocks_InMarkdown, which then calls
+ # _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1"
+ # attribute is found whitin a tag, _HashHTMLBlocks_InHTML calls back
+ # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag.
+ # These two functions are calling each other. It's recursive!
+ #
+ #
+ # Call the HTML-in-Markdown hasher.
+ #
+ list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text);
+
+ return $text;
+ }
+ function _hashHTMLBlocks_inMarkdown($text, $indent = 0,
+ $enclosing_tag_re = '', $span = false)
+ {
+ #
+ # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags.
+ #
+ # * $indent is the number of space to be ignored when checking for code
+ # blocks. This is important because if we don't take the indent into
+ # account, something like this (which looks right) won't work as expected:
+ #
+ #
+ #
+ # Hello World. <-- Is this a Markdown code block or text?
+ #
<-- Is this a Markdown code block or a real tag?
+ #
+ #
+ # If you don't like this, just don't indent the tag on which
+ # you apply the markdown="1" attribute.
+ #
+ # * If $enclosing_tag_re is not empty, stops at the first unmatched closing
+ # tag with that name. Nested tags supported.
+ #
+ # * If $span is true, text inside must treated as span. So any double
+ # newline will be replaced by a single newline so that it does not create
+ # paragraphs.
+ #
+ # Returns an array of that form: ( processed text , remaining text )
+ #
+ if ($text === '') return array('', '');
+
+ # Regex to check for the presense of newlines around a block tag.
+ $newline_before_re = '/(?:^\n?|\n\n)*$/';
+ $newline_after_re =
+ '{
+ ^ # Start of text following the tag.
+ (?>[ ]*)? # Optional comment.
+ [ ]*\n # Must be followed by newline.
+ }xs';
+
+ # Regex to match any tag.
+ $block_tag_re =
+ '{
+ ( # $2: Capture hole tag.
+ ? # Any opening or closing tag.
+ (?> # Tag name.
+ '.$this->block_tags_re.' |
+ '.$this->context_block_tags_re.' |
+ '.$this->clean_tags_re.' |
+ (?!\s)'.$enclosing_tag_re.'
+ )
+ (?:
+ (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name.
+ (?>
+ ".*?" | # Double quotes (can contain `>`)
+ \'.*?\' | # Single quotes (can contain `>`)
+ .+? # Anything but quotes and `>`.
+ )*?
+ )?
+ > # End of tag.
+ |
+ # HTML Comment
+ |
+ <\?.*?\?> | <%.*?%> # Processing instruction
+ |
+ # CData Block
+ |
+ # Code span marker
+ `+
+ '. ( !$span ? ' # If not in span.
+ |
+ # Indented code block
+ (?: ^[ ]*\n | ^ | \n[ ]*\n )
+ [ ]{'.($indent+4).'}[^\n]* \n
+ (?>
+ (?: [ ]{'.($indent+4).'}[^\n]* | [ ]* ) \n
+ )*
+ |
+ # Fenced code block marker
+ (?> ^ | \n )
+ [ ]{0,'.($indent).'}~~~+[ ]*\n
+ ' : '' ). ' # End (if not is span).
+ )
+ }xs';
+
+
+ $depth = 0; # Current depth inside the tag tree.
+ $parsed = ""; # Parsed text that will be returned.
+
+ #
+ # Loop through every tag until we find the closing tag of the parent
+ # or loop until reaching the end of text if no parent tag specified.
+ #
+ do {
+ #
+ # Split the text using the first $tag_match pattern found.
+ # Text before pattern will be first in the array, text after
+ # pattern will be at the end, and between will be any catches made
+ # by the pattern.
+ #
+ $parts = preg_split($block_tag_re, $text, 2,
+ PREG_SPLIT_DELIM_CAPTURE);
+
+ # If in Markdown span mode, add a empty-string span-level hash
+ # after each newline to prevent triggering any block element.
+ if ($span) {
+ $void = $this->hashPart("", ':');
+ $newline = "$void\n";
+ $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void;
+ }
+
+ $parsed .= $parts[0]; # Text before current tag.
+
+ # If end of $text has been reached. Stop loop.
+ if (count($parts) < 3) {
+ $text = "";
+ break;
+ }
+
+ $tag = $parts[1]; # Tag to handle.
+ $text = $parts[2]; # Remaining text after current tag.
+ $tag_re = preg_quote($tag); # For use in a regular expression.
+
+ #
+ # Check for: Code span marker
+ #
+ if ($tag{0} == "`") {
+ # Find corresponding end marker.
+ $tag_re = preg_quote($tag);
+ if (preg_match('{^(?>.+?|\n(?!\n))*?(?.*\n)+?[ ]{0,'.($indent).'}'.$tag_re.'[ ]*\n}', $text,
+ $matches))
+ {
+ # End marker found: pass text unchanged until marker.
+ $parsed .= $tag . $matches[0];
+ $text = substr($text, strlen($matches[0]));
+ }
+ else {
+ # No end marker: just skip it.
+ $parsed .= $tag;
+ }
+ }
+ #
+ # Check for: Indented code block.
+ #
+ else if ($tag{0} == "\n" || $tag{0} == " ") {
+ # Indented code block: pass it unchanged, will be handled
+ # later.
+ $parsed .= $tag;
+ }
+ #
+ # Check for: Opening Block level tag or
+ # Opening Context Block tag (like ins and del)
+ # used as a block tag (tag is alone on it's line).
+ #
+ else if (preg_match('{^<(?:'.$this->block_tags_re.')\b}', $tag) ||
+ ( preg_match('{^<(?:'.$this->context_block_tags_re.')\b}', $tag) &&
+ preg_match($newline_before_re, $parsed) &&
+ preg_match($newline_after_re, $text) )
+ )
+ {
+ # Need to parse tag and following text using the HTML parser.
+ list($block_text, $text) =
+ $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true);
+
+ # Make sure it stays outside of any paragraph by adding newlines.
+ $parsed .= "\n\n$block_text\n\n";
+ }
+ #
+ # Check for: Clean tag (like script, math)
+ # HTML Comments, processing instructions.
+ #
+ else if (preg_match('{^<(?:'.$this->clean_tags_re.')\b}', $tag) ||
+ $tag{1} == '!' || $tag{1} == '?')
+ {
+ # Need to parse tag and following text using the HTML parser.
+ # (don't check for markdown attribute)
+ list($block_text, $text) =
+ $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false);
+
+ $parsed .= $block_text;
+ }
+ #
+ # Check for: Tag with same name as enclosing tag.
+ #
+ else if ($enclosing_tag_re !== '' &&
+ # Same name as enclosing tag.
+ preg_match('{^?(?:'.$enclosing_tag_re.')\b}', $tag))
+ {
+ #
+ # Increase/decrease nested tag count.
+ #
+ if ($tag{1} == '/') $depth--;
+ else if ($tag{strlen($tag)-2} != '/') $depth++;
+
+ if ($depth < 0) {
+ #
+ # Going out of parent element. Clean up and break so we
+ # return to the calling function.
+ #
+ $text = $tag . $text;
+ break;
+ }
+
+ $parsed .= $tag;
+ }
+ else {
+ $parsed .= $tag;
+ }
+ } while ($depth >= 0);
+
+ return array($parsed, $text);
+ }
+ function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) {
+ #
+ # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags.
+ #
+ # * Calls $hash_method to convert any blocks.
+ # * Stops when the first opening tag closes.
+ # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed.
+ # (it is not inside clean tags)
+ #
+ # Returns an array of that form: ( processed text , remaining text )
+ #
+ if ($text === '') return array('', '');
+
+ # Regex to match `markdown` attribute inside of a tag.
+ $markdown_attr_re = '
+ {
+ \s* # Eat whitespace before the `markdown` attribute
+ markdown
+ \s*=\s*
+ (?>
+ (["\']) # $1: quote delimiter
+ (.*?) # $2: attribute value
+ \1 # matching delimiter
+ |
+ ([^\s>]*) # $3: unquoted attribute value
+ )
+ () # $4: make $3 always defined (avoid warnings)
+ }xs';
+
+ # Regex to match any tag.
+ $tag_re = '{
+ ( # $2: Capture hole tag.
+ ? # Any opening or closing tag.
+ [\w:$]+ # Tag name.
+ (?:
+ (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name.
+ (?>
+ ".*?" | # Double quotes (can contain `>`)
+ \'.*?\' | # Single quotes (can contain `>`)
+ .+? # Anything but quotes and `>`.
+ )*?
+ )?
+ > # End of tag.
+ |
+ # HTML Comment
+ |
+ <\?.*?\?> | <%.*?%> # Processing instruction
+ |
+ # CData Block
+ )
+ }xs';
+
+ $original_text = $text; # Save original text in case of faliure.
+
+ $depth = 0; # Current depth inside the tag tree.
+ $block_text = ""; # Temporary text holder for current text.
+ $parsed = ""; # Parsed text that will be returned.
+
+ #
+ # Get the name of the starting tag.
+ # (This pattern makes $base_tag_name_re safe without quoting.)
+ #
+ if (preg_match('/^<([\w:$]*)\b/', $text, $matches))
+ $base_tag_name_re = $matches[1];
+
+ #
+ # Loop through every tag until we find the corresponding closing tag.
+ #
+ do {
+ #
+ # Split the text using the first $tag_match pattern found.
+ # Text before pattern will be first in the array, text after
+ # pattern will be at the end, and between will be any catches made
+ # by the pattern.
+ #
+ $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
+
+ if (count($parts) < 3) {
+ #
+ # End of $text reached with unbalenced tag(s).
+ # In that case, we return original text unchanged and pass the
+ # first character as filtered to prevent an infinite loop in the
+ # parent function.
+ #
+ return array($original_text{0}, substr($original_text, 1));
+ }
+
+ $block_text .= $parts[0]; # Text before current tag.
+ $tag = $parts[1]; # Tag to handle.
+ $text = $parts[2]; # Remaining text after current tag.
+
+ #
+ # Check for: Auto-close tag (like )
+ # Comments and Processing Instructions.
+ #
+ if (preg_match('{^?(?:'.$this->auto_close_tags_re.')\b}', $tag) ||
+ $tag{1} == '!' || $tag{1} == '?')
+ {
+ # Just add the tag to the block as if it was text.
+ $block_text .= $tag;
+ }
+ else {
+ #
+ # Increase/decrease nested tag count. Only do so if
+ # the tag's name match base tag's.
+ #
+ if (preg_match('{^?'.$base_tag_name_re.'\b}', $tag)) {
+ if ($tag{1} == '/') $depth--;
+ else if ($tag{strlen($tag)-2} != '/') $depth++;
+ }
+
+ #
+ # Check for `markdown="1"` attribute and handle it.
+ #
+ if ($md_attr &&
+ preg_match($markdown_attr_re, $tag, $attr_m) &&
+ preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3]))
+ {
+ # Remove `markdown` attribute from opening tag.
+ $tag = preg_replace($markdown_attr_re, '', $tag);
+
+ # Check if text inside this tag must be parsed in span mode.
+ $this->mode = $attr_m[2] . $attr_m[3];
+ $span_mode = $this->mode == 'span' || $this->mode != 'block' &&
+ preg_match('{^<(?:'.$this->contain_span_tags_re.')\b}', $tag);
+
+ # Calculate indent before tag.
+ if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) {
+ $strlen = $this->utf8_strlen;
+ $indent = $strlen($matches[1], 'UTF-8');
+ } else {
+ $indent = 0;
+ }
+
+ # End preceding block with this tag.
+ $block_text .= $tag;
+ $parsed .= $this->$hash_method($block_text);
+
+ # Get enclosing tag name for the ParseMarkdown function.
+ # (This pattern makes $tag_name_re safe without quoting.)
+ preg_match('/^<([\w:$]*)\b/', $tag, $matches);
+ $tag_name_re = $matches[1];
+
+ # Parse the content using the HTML-in-Markdown parser.
+ list ($block_text, $text)
+ = $this->_hashHTMLBlocks_inMarkdown($text, $indent,
+ $tag_name_re, $span_mode);
+
+ # Outdent markdown text.
+ if ($indent > 0) {
+ $block_text = preg_replace("/^[ ]{1,$indent}/m", "",
+ $block_text);
+ }
+
+ # Append tag content to parsed text.
+ if (!$span_mode) $parsed .= "\n\n$block_text\n\n";
+ else $parsed .= "$block_text";
+
+ # Start over a new block.
+ $block_text = "";
+ }
+ else $block_text .= $tag;
+ }
+
+ } while ($depth > 0);
+
+ #
+ # Hash last block text that wasn't processed inside the loop.
+ #
+ $parsed .= $this->$hash_method($block_text);
+
+ return array($parsed, $text);
+ }
+
+
+ function hashClean($text) {
+ #
+ # Called whenever a tag must be hashed when a function insert a "clean" tag
+ # in $text, it pass through this function and is automaticaly escaped,
+ # blocking invalid nested overlap.
+ #
+ return $this->hashPart($text, 'C');
+ }
+
+
+ function doHeaders($text) {
+ #
+ # Redefined to add id attribute support.
+ #
+ # Setext-style headers:
+ # Header 1 {#header1}
+ # ========
+ #
+ # Header 2 {#header2}
+ # --------
+ #
+ $text = preg_replace_callback(
+ '{
+ (^.+?) # $1: Header text
+ (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? # $2: Id attribute
+ [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer
+ }mx',
+ array(&$this, '_doHeaders_callback_setext'), $text);
+
+ # atx-style headers:
+ # # Header 1 {#header1}
+ # ## Header 2 {#header2}
+ # ## Header 2 with closing hashes ## {#header3}
+ # ...
+ # ###### Header 6 {#header2}
+ #
+ $text = preg_replace_callback('{
+ ^(\#{1,6}) # $1 = string of #\'s
+ [ ]*
+ (.+?) # $2 = Header text
+ [ ]*
+ \#* # optional closing #\'s (not counted)
+ (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? # id attribute
+ [ ]*
+ \n+
+ }xm',
+ array(&$this, '_doHeaders_callback_atx'), $text);
+
+ return $text;
+ }
+ function _doHeaders_attr($attr) {
+ if (empty($attr)) return "";
+ return " id=\"$attr\"";
+ }
+ function _doHeaders_callback_setext($matches) {
+ if ($matches[3] == '-' && preg_match('{^- }', $matches[1]))
+ return $matches[0];
+ $level = $matches[3]{0} == '=' ? 1 : 2;
+ $attr = $this->_doHeaders_attr($id =& $matches[2]);
+ $block = "".$this->runSpanGamut($matches[1])."";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+ function _doHeaders_callback_atx($matches) {
+ $level = strlen($matches[1]);
+ $attr = $this->_doHeaders_attr($id =& $matches[3]);
+ $block = "".$this->runSpanGamut($matches[2])."";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+
+
+ function doTables($text) {
+ #
+ # Form HTML tables.
+ #
+ $less_than_tab = $this->tab_width - 1;
+ #
+ # Find tables with leading pipe.
+ #
+ # | Header 1 | Header 2
+ # | -------- | --------
+ # | Cell 1 | Cell 2
+ # | Cell 3 | Cell 4
+ #
+ $text = preg_replace_callback('
+ {
+ ^ # Start of a line
+ [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
+ [|] # Optional leading pipe (present)
+ (.+) \n # $1: Header row (at least one pipe)
+
+ [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
+ [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline
+
+ ( # $3: Cells
+ (?>
+ [ ]* # Allowed whitespace.
+ [|] .* \n # Row content.
+ )*
+ )
+ (?=\n|\Z) # Stop at final double newline.
+ }xm',
+ array(&$this, '_doTable_leadingPipe_callback'), $text);
+
+ #
+ # Find tables without leading pipe.
+ #
+ # Header 1 | Header 2
+ # -------- | --------
+ # Cell 1 | Cell 2
+ # Cell 3 | Cell 4
+ #
+ $text = preg_replace_callback('
+ {
+ ^ # Start of a line
+ [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
+ (\S.*[|].*) \n # $1: Header row (at least one pipe)
+
+ [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
+ ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline
+
+ ( # $3: Cells
+ (?>
+ .* [|] .* \n # Row content
+ )*
+ )
+ (?=\n|\Z) # Stop at final double newline.
+ }xm',
+ array(&$this, '_DoTable_callback'), $text);
+
+ return $text;
+ }
+ function _doTable_leadingPipe_callback($matches) {
+ $head = $matches[1];
+ $underline = $matches[2];
+ $content = $matches[3];
+
+ # Remove leading pipe for each row.
+ $content = preg_replace('/^ *[|]/m', '', $content);
+
+ return $this->_doTable_callback(array($matches[0], $head, $underline, $content));
+ }
+ function _doTable_callback($matches) {
+ $head = $matches[1];
+ $underline = $matches[2];
+ $content = $matches[3];
+
+ # Remove any tailing pipes for each line.
+ $head = preg_replace('/[|] *$/m', '', $head);
+ $underline = preg_replace('/[|] *$/m', '', $underline);
+ $content = preg_replace('/[|] *$/m', '', $content);
+
+ # Reading alignement from header underline.
+ $separators = preg_split('/ *[|] */', $underline);
+ foreach ($separators as $n => $s) {
+ if (preg_match('/^ *-+: *$/', $s)) $attr[$n] = ' align="right"';
+ else if (preg_match('/^ *:-+: *$/', $s))$attr[$n] = ' align="center"';
+ else if (preg_match('/^ *:-+ *$/', $s)) $attr[$n] = ' align="left"';
+ else $attr[$n] = '';
+ }
+
+ # Parsing span elements, including code spans, character escapes,
+ # and inline HTML tags, so that pipes inside those gets ignored.
+ $head = $this->parseSpan($head);
+ $headers = preg_split('/ *[|] */', $head);
+ $col_count = count($headers);
+
+ # Write column headers.
+ $text = "
";
+
+ return $this->hashBlock($text) . "\n";
+ }
+
+
+ function doDefLists($text) {
+ #
+ # Form HTML definition lists.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Re-usable pattern to match any entire dl list:
+ $whole_list_re = '(?>
+ ( # $1 = whole list
+ ( # $2
+ [ ]{0,'.$less_than_tab.'}
+ ((?>.*\S.*\n)+) # $3 = defined term
+ \n?
+ [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
+ )
+ (?s:.+?)
+ ( # $4
+ \z
+ |
+ \n{2,}
+ (?=\S)
+ (?! # Negative lookahead for another term
+ [ ]{0,'.$less_than_tab.'}
+ (?: \S.*\n )+? # defined term
+ \n?
+ [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
+ )
+ (?! # Negative lookahead for another definition
+ [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
+ )
+ )
+ )
+ )'; // mx
+
+ $text = preg_replace_callback('{
+ (?>\A\n?|(?<=\n\n))
+ '.$whole_list_re.'
+ }mx',
+ array(&$this, '_doDefLists_callback'), $text);
+
+ return $text;
+ }
+ function _doDefLists_callback($matches) {
+ # Re-usable patterns to match list item bullets and number markers:
+ $list = $matches[1];
+
+ # Turn double returns into triple returns, so that we can make a
+ # paragraph for the last item in a list, if necessary:
+ $result = trim($this->processDefListItems($list));
+ $result = "
\n" . $result . "\n
";
+ return $this->hashBlock($result) . "\n\n";
+ }
+
+
+ function processDefListItems($list_str) {
+ #
+ # Process the contents of a single definition list, splitting it
+ # into individual term and definition list items.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # trim trailing blank lines:
+ $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
+
+ # Process definition terms.
+ $list_str = preg_replace_callback('{
+ (?>\A\n?|\n\n+) # leading line
+ ( # definition terms = $1
+ [ ]{0,'.$less_than_tab.'} # leading whitespace
+ (?![:][ ]|[ ]) # negative lookahead for a definition
+ # mark (colon) or more whitespace.
+ (?> \S.* \n)+? # actual term (not whitespace).
+ )
+ (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed
+ # with a definition mark.
+ }xm',
+ array(&$this, '_processDefListItems_callback_dt'), $list_str);
+
+ # Process actual definitions.
+ $list_str = preg_replace_callback('{
+ \n(\n+)? # leading line = $1
+ ( # marker space = $2
+ [ ]{0,'.$less_than_tab.'} # whitespace before colon
+ [:][ ]+ # definition mark (colon)
+ )
+ ((?s:.+?)) # definition text = $3
+ (?= \n+ # stop at next definition mark,
+ (?: # next term or end of text
+ [ ]{0,'.$less_than_tab.'} [:][ ] |
+
";
+ return "\n\n".$this->hashBlock($codeblock)."\n\n";
+ }
+ function _doFencedCodeBlocks_newlines($matches) {
+ return str_repeat(" empty_element_suffix",
+ strlen($matches[0]));
+ }
+
+
+ #
+ # Redefining emphasis markers so that emphasis by underscore does not
+ # work in the middle of a word.
+ #
+ var $em_relist = array(
+ '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? tags
+ #
+ # Strip leading and trailing lines:
+ $text = preg_replace('/\A\n+|\n+\z/', '', $text);
+
+ $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
+
+ #
+ # Wrap
tags and unhashify HTML blocks
+ #
+ foreach ($grafs as $key => $value) {
+ $value = trim($this->runSpanGamut($value));
+
+ # Check if this should be enclosed in a paragraph.
+ # Clean tag hashes & block tag hashes are left alone.
+ $is_p = !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value);
+
+ if ($is_p) {
+ $value = "
$value
";
+ }
+ $grafs[$key] = $value;
+ }
+
+ # Join grafs in one text, then unhash HTML tags.
+ $text = implode("\n\n", $grafs);
+
+ # Finish by removing any tag hashes still present in $text.
+ $text = $this->unhash($text);
+
+ return $text;
+ }
+
+
+ ### Footnotes
+
+ function stripFootnotes($text) {
+ #
+ # Strips link definitions from text, stores the URLs and titles in
+ # hash references.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Link defs are in the form: [^id]: url "optional title"
+ $text = preg_replace_callback('{
+ ^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?: # note_id = $1
+ [ ]*
+ \n? # maybe *one* newline
+ ( # text = $2 (no blank lines allowed)
+ (?:
+ .+ # actual text
+ |
+ \n # newlines but
+ (?!\[\^.+?\]:\s)# negative lookahead for footnote marker.
+ (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed
+ # by non-indented content
+ )*
+ )
+ }xm',
+ array(&$this, '_stripFootnotes_callback'),
+ $text);
+ return $text;
+ }
+ function _stripFootnotes_callback($matches) {
+ $note_id = $this->fn_id_prefix . $matches[1];
+ $this->footnotes[$note_id] = $this->outdent($matches[2]);
+ return ''; # String that will replace the block
+ }
+
+
+ function doFootnotes($text) {
+ #
+ # Replace footnote references in $text [^id] with a special text-token
+ # which will be replaced by the actual footnote marker in appendFootnotes.
+ #
+ if (!$this->in_anchor) {
+ $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text);
+ }
+ return $text;
+ }
+
+
+ function appendFootnotes($text) {
+ #
+ # Append footnote list to text.
+ #
+ $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
+ array(&$this, '_appendFootnotes_callback'), $text);
+
+ if (!empty($this->footnotes_ordered)) {
+ $text .= "\n\n";
+ $text .= "
";
+ }
+ return $text;
+ }
+ function _appendFootnotes_callback($matches) {
+ $node_id = $this->fn_id_prefix . $matches[1];
+
+ # Create footnote marker only if it has a corresponding footnote *and*
+ # the footnote hasn't been used by another marker.
+ if (isset($this->footnotes[$node_id])) {
+ # Transfert footnote content to the ordered list.
+ $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id];
+ unset($this->footnotes[$node_id]);
+
+ $num = $this->footnote_counter++;
+ $attr = " rel=\"footnote\"";
+ if ($this->fn_link_class != "") {
+ $class = $this->fn_link_class;
+ $class = $this->encodeAttribute($class);
+ $attr .= " class=\"$class\"";
+ }
+ if ($this->fn_link_title != "") {
+ $title = $this->fn_link_title;
+ $title = $this->encodeAttribute($title);
+ $attr .= " title=\"$title\"";
+ }
+
+ $attr = str_replace("%%", $num, $attr);
+ $node_id = $this->encodeAttribute($node_id);
+
+ return
+ "".
+ "$num".
+ "";
+ }
+
+ return "[^".$matches[1]."]";
+ }
+
+
+ ### Abbreviations ###
+
+ function stripAbbreviations($text) {
+ #
+ # Strips abbreviations from text, stores titles in hash references.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Link defs are in the form: [id]*: url "optional title"
+ $text = preg_replace_callback('{
+ ^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1
+ (.*) # text = $2 (no blank lines allowed)
+ }xm',
+ array(&$this, '_stripAbbreviations_callback'),
+ $text);
+ return $text;
+ }
+ function _stripAbbreviations_callback($matches) {
+ $abbr_word = $matches[1];
+ $abbr_desc = $matches[2];
+ if ($this->abbr_word_re)
+ $this->abbr_word_re .= '|';
+ $this->abbr_word_re .= preg_quote($abbr_word);
+ $this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
+ return ''; # String that will replace the block
+ }
+
+
+ function doAbbreviations($text) {
+ #
+ # Find defined abbreviations in text and wrap them in elements.
+ #
+ if ($this->abbr_word_re) {
+ // cannot use the /x modifier because abbr_word_re may
+ // contain significant spaces:
+ $text = preg_replace_callback('{'.
+ '(?abbr_word_re.')'.
+ '(?![\w\x1A])'.
+ '}',
+ array(&$this, '_doAbbreviations_callback'), $text);
+ }
+ return $text;
+ }
+ function _doAbbreviations_callback($matches) {
+ $abbr = $matches[0];
+ if (isset($this->abbr_desciptions[$abbr])) {
+ $desc = $this->abbr_desciptions[$abbr];
+ if (empty($desc)) {
+ return $this->hashPart("$abbr");
+ } else {
+ $desc = $this->encodeAttribute($desc);
+ return $this->hashPart("$abbr");
+ }
+ } else {
+ return $matches[0];
+ }
+ }
+
+}
+
+
+/*
+
+PHP Markdown Extra
+==================
+
+Description
+-----------
+
+This is a PHP port of the original Markdown formatter written in Perl
+by John Gruber. This special "Extra" version of PHP Markdown features
+further enhancements to the syntax for making additional constructs
+such as tables and definition list.
+
+Markdown is a text-to-HTML filter; it translates an easy-to-read /
+easy-to-write structured text format into HTML. Markdown's text format
+is most similar to that of plain text email, and supports features such
+as headers, *emphasis*, code blocks, blockquotes, and links.
+
+Markdown's syntax is designed not as a generic markup language, but
+specifically to serve as a front-end to (X)HTML. You can use span-level
+HTML tags anywhere in a Markdown document, and you can use block level
+HTML tags (like
and
as well).
+
+For more information about Markdown's syntax, see:
+
+
+
+
+Bugs
+----
+
+To file bug reports please send email to:
+
+
+
+Please include with your report: (1) the example input; (2) the output you
+expected; (3) the output Markdown actually produced.
+
+
+Version History
+---------------
+
+See the readme file for detailed release notes for this version.
+
+
+Copyright and License
+---------------------
+
+PHP Markdown & Extra
+Copyright (c) 2004-2009 Michel Fortin
+
+All rights reserved.
+
+Based on Markdown
+Copyright (c) 2003-2006 John Gruber
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name "Markdown" nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+This software is provided by the copyright holders and contributors "as
+is" and any express or implied warranties, including, but not limited
+to, the implied warranties of merchantability and fitness for a
+particular purpose are disclaimed. In no event shall the copyright owner
+or contributors be liable for any direct, indirect, incidental, special,
+exemplary, or consequential damages (including, but not limited to,
+procurement of substitute goods or services; loss of use, data, or
+profits; or business interruption) however caused and on any theory of
+liability, whether in contract, strict liability, or tort (including
+negligence or otherwise) arising in any way out of the use of this
+software, even if advised of the possibility of such damage.
+
+*/
+
+
+
+
+/**
+ * MarkdownExtra for Feather, passes URLs through the URL class.
+ */
+class MarkdownFeather_Parser extends MarkdownExtra_Parser {
+
+ /**
+ * Extended to pass URLs through Laravel.
+ *
+ * @internal
+ * @param array $matches
+ * @return string
+ * @uses Laravel\URL::to()
+ */
+ function _doAnchors_inline_callback($matches)
+ {
+ $link_text = $this->runSpanGamut($matches[2]);
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
+ $title = isset($matches[7]) ? $matches[7] : null;
+
+ $url = $this->encodeAttribute($url);
+
+ // BEGIN: Modification to pass URLs through Laravel
+ if ($url[0] !== '#' and is_null(parse_url($url, PHP_URL_SCHEME)))
+ {
+ $url = \Feather\URL::to($url);
+ }
+ // END
+
+ $result = "encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+
+ $link_text = $this->runSpanGamut($link_text);
+ $result .= ">$link_text";
+
+ return $this->hashPart($result);
+ }
+
+ /**
+ * Extended to pass URLs through Laravel.
+ *
+ * @internal
+ * @param array $matches
+ * @return string
+ * @uses Laravel\URL::to_asset()
+ */
+ function _doImages_inline_callback($matches)
+ {
+ $alt_text = $matches[2];
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
+ $title = isset($matches[7]) ? $matches[7] : null;
+
+ $alt_text = $this->encodeAttribute($alt_text);
+ $url = $this->encodeAttribute($url);
+
+ // BEGIN: Modification to pass URLs through Laravel
+ if ($url[0] !== '#' and is_null(parse_url($url, PHP_URL_SCHEME)))
+ {
+ $url = \Feather\URL::to_asset($url);
+ }
+ // END
+
+ $result = "encodeAttribute($title);
+ $result .= " title=\"$title\""; # $title already quoted
+ }
+ $result .= $this->empty_element_suffix;
+
+ return $this->hashPart($result);
+ }
+
+}
diff --git a/gears/oneconnect/gear.json b/gears/oneconnect/gear.json
new file mode 100644
index 0000000..c61efe5
--- /dev/null
+++ b/gears/oneconnect/gear.json
@@ -0,0 +1,12 @@
+{
+ "name":"OneConnect",
+ "description":"A Single Sign On gear to connect your application to your Feather installation.",
+ "version":"1.0.0",
+ "author":
+ {
+ "name":"Jason Lewis",
+ "email":"jason.lewis1991@gmail.com",
+ "website":"http://jasonlewis.me"
+ },
+ "dependencies":{}
+}
\ No newline at end of file
diff --git a/gears/oneconnect/oneconnect.gear.php b/gears/oneconnect/oneconnect.gear.php
new file mode 100644
index 0000000..e2214f5
--- /dev/null
+++ b/gears/oneconnect/oneconnect.gear.php
@@ -0,0 +1,147 @@
+listen('auth: bootstrap oneconnect', 'bootstrap');
+ }
+
+ /**
+ * Bootstrap the OneConnect plugin.
+ *
+ * @return mixed
+ */
+ public function bootstrap()
+ {
+ try
+ {
+ $this->compatible();
+ }
+ catch (Exception $error)
+ {
+ return;
+ }
+
+ if($credentials = $this->{$this->handler}())
+ {
+ return $this->feather['sso']->authorize((array) $credentials);
+ }
+ }
+
+ /**
+ * Fetch the authentication data using cURL.
+ *
+ * @return mixed
+ */
+ protected function curl()
+ {
+ $ch = curl_init($this->feather['config']->get('feather: db.auth.authenticate_url'));
+
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_COOKIE, $this->cookies());
+
+ $credentials = json_decode(curl_exec($ch));
+
+ curl_close($ch);
+
+ return $credentials;
+ }
+
+ /**
+ * Fetch the authentication data using file_get_contents().
+ *
+ * @return mixed
+ */
+ protected function file()
+ {
+ $opts = array(
+ 'http' => array(
+ 'method' => 'GET',
+ 'header' => 'Cookie: ' . $this->cookies()
+ )
+ );
+
+ $context = stream_context_create($opts);
+
+ $credentials = json_decode(file_get_contents($this->feather['config']->get('feather: db.auth.authenticate_url'), false, $context));
+
+ return $credentials;
+ }
+
+ /**
+ * Returns a cookie string to be sent along with headers.
+ *
+ * @return string
+ */
+ protected function cookies()
+ {
+ $cookies = array();
+
+ foreach(Request::foundation()->cookies->all() as $key => $value)
+ {
+ $cookies[] = "{$key}={$value}";
+ }
+
+ return implode('; ', $cookies);
+ }
+
+ /**
+ * OneConnect needs either cURL or file_get_contents to fetch the authentication data.
+ * When activating we make sure the plugin is compatible with the environment.
+ *
+ * @return void
+ */
+ protected function compatible()
+ {
+ if(!function_exists('curl_init'))
+ {
+ if(!function_exists('file_get_contents') or !ini_get('allow_url_fopen'))
+ {
+ throw new Exception('OneConnect is not compatible with your environment, cURL or allow_url_fopen is required.');
+ }
+ else
+ {
+ $this->handler = 'file';
+ }
+ }
+ else
+ {
+ $this->handler = 'curl';
+ }
+ }
+
+ /**
+ * Runs when the OneConnect plugin is activated.
+ *
+ * @return bool
+ */
+ public function activate()
+ {
+ try
+ {
+ $this->compatible();
+ }
+ catch (Exception $error)
+ {
+ throw $error;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/gears/oneconnect/wordpress/README.md b/gears/oneconnect/wordpress/README.md
new file mode 100644
index 0000000..41f3f9d
--- /dev/null
+++ b/gears/oneconnect/wordpress/README.md
@@ -0,0 +1,32 @@
+# Feather OneConnect for WordPress
+
+Feather OneConnect for WordPress enables Single Sign On between your Feather installation and WordPress. Your WordPress user base can sign in and be automatically authenticated when they visit your Feather installation.
+
+### Installation
+
+Installing Feather OneConnect for WordPress is dead easy.
+
+1. Copy and paste the `feather-oneconnect.php` file to your WordPress plugin directory.
+2. Enable the plugin in your WordPress administration panel.
+3. Define your cookie path and domain in your `wp-config.php` file. Here's an example.
+
+ // The cookie path should be just a slash.
+ define('COOKIEPATH', '/');
+
+ // The cookie domain should be the same as the one in your Feather installation.
+ define('COOKIE_DOMAIN', '.mydomain.com');
+
+4. Enable OneConnect on your Feather installation.
+5. Copy the Authenticate, Registration, Login, and Logout URLs from the Feather OneConnect for WordPress configuration screen and paste them into the corresponding boxes on the OneConnect configuration.
+
+When you next login to WordPress you'll become authenticated on your Feather installation. If something isn't working properly try deleting your cookies and starting fresh!
+
+### How It Works
+
+Feather OneConnect for WordPress only works when both WordPress and your Feather installation are on the same domain. This is because cookies are not available across domains.
+
+When a user logs in to WordPress they are given a cookie which is stored on their local machine. When the user visits Feather, OneConnect is able to access all the cookies for the current domain, so this includes the WordPress cookies. OneConnect then makes a request to your WordPress installation, it passes along the cookie information so that WordPress can authenticate the current user. WordPress then returns a couple bits of information about the user to OneConnect.
+
+Once OneConnect has this information it attempts to authorize the user on Feather. If the user has never visited the forum before, OneConnect will attempt to create a new account for the user with the same username and e-mail they have on WordPress. If, for some crazy reason, either of those is taken then OneConnect will show the user a form. The user then has the chance to create a new account, or link themselves to an existing account. Once done the user will be authenticated and can now access the forums. If the e-mail and username are available then the user will be immediately authenticated.
+
+This all happens very quickly and unless the user is prompted to create or link an account they won't even realise it's happening.
\ No newline at end of file
diff --git a/gears/oneconnect/wordpress/feather-oneconnect.php b/gears/oneconnect/wordpress/feather-oneconnect.php
new file mode 100644
index 0000000..557e407
--- /dev/null
+++ b/gears/oneconnect/wordpress/feather-oneconnect.php
@@ -0,0 +1,213 @@
+authenticated();
+ }
+
+ // Create the admin menu interface for our plugin.
+ if(is_admin())
+ {
+ add_action('admin_menu', array($this, 'menu'));
+ }
+
+ add_action('clear_auth_cookie', array($this, 'logout'));
+
+ register_activation_hook(__FILE__, array($this, 'activate'));
+ register_deactivation_hook(__FILE__, array($this, 'deactivate'));
+ }
+
+ /**
+ * Logs the user out of Feather as well.
+ *
+ * @return void
+ */
+ public function logout()
+ {
+ setcookie($this->cookie, ' ', time() - 31536000, COOKIEPATH, COOKIE_DOMAIN);
+ }
+
+ /**
+ * Adds the plugin to the plugins menu.
+ *
+ * @return void
+ */
+ public function menu()
+ {
+ add_plugins_page(
+ 'Feather OneConnect',
+ 'Feather OneConnect',
+ 'administrator',
+ 'oneconnect',
+ array($this, 'options')
+ );
+ }
+
+ /**
+ * The options page for the OneConnect plugin.
+ *
+ * @return string
+ */
+ public function options()
+ {
+ if(isset($_POST['save'])) {
+ if(function_exists('current_user_can') && !current_user_can('manage_options'))
+ {
+ die(__('Permission Denied'));
+ }
+
+ $authenticate_url = str_replace('{token}', $this->token(), $_POST['feather_oneconnect_authenticate']);
+
+ update_option('feather_oneconnect_authenticate', $authenticate_url);
+ }
+ else
+ {
+ $authenticate_url = get_option('feather_oneconnect_authenticate');
+ }
+
+ echo '
The authenticate URL is set by default, however you can change this URL if the default is already in use. Use the {token} placeholder to generate a random token string.
The following information needs to be copied and pasted over to the OneConnect options on your Feather installation.';
+ echo ' Remember that if you changed the Authenticate URL above you need to save it first to get the updated URL.