Upgrading from 4.2.12 to 4.3.0
Please refer to the upgrade instructions corresponding to your installation method.
Composer Version
Important
If you use Composer, CodeIgniter v4.3.0 requires Composer 2.0.14 or later.
If you are using older version of Composer, upgrade your composer
tool,
and delete the vendor/ directory, and run composer update
again.
The procedure, for example, is as follows:
composer self-update
rm -rf vendor/
composer update
Mandatory File Changes
spark
The following files received significant changes and you must merge the updated versions with your application:
spark
Important
If you do not update this file, Spark commands will not work at all after running composer update
.
The upgrade procedure, for example, is as follows:
composer update
cp vendor/codeigniter4/framework/spark .
Config Files
app/Config/Kint.php
app/Config/Kint.php has been updated for Kint 5.0.
You need to replace:
Kint\Renderer\Renderer
withKint\Renderer\AbstractRenderer
Renderer::SORT_FULL
withAbstractRenderer::SORT_FULL
app/Config/Exceptions.php
If you are using PHP 8.2, you need to add new properties
$logDeprecations
and$deprecationLogLevel
.
Mock Config Classes
If you are using the following Mock Config classes in testing, you need to update the corresponding Config files in app/Config:
MockAppConfig
(Config\App
)MockCLIConfig
(Config\App
)MockSecurityConfig
(Config\Security
)
Add types to the properties in these Config classes. You may need to fix the property values to match the property types.
composer.json
If you installed CodeIgnter manually, and are using Composer,
you need to remove the following lines, and run composer update
.
{
...
"require": {
...
"kint-php/kint": "^4.2", <-- Remove this line
...
},
...
"scripts": {
"post-update-cmd": [
"CodeIgniter\\ComposerScripts::postUpdate" <-- Remove this line
],
"test": "phpunit"
},
...
}
Breaking Changes
Database Exception Changes
The exception classes may be changed when database errors occur. If you catch the exceptions, you must confirm that your code can catch the exceptions.
Now a few exceptions will be thrown even if
CI_DEBUG
is false.During transactions, exceptions are not thrown by default even if
DBDebug
is true. If you want exceptions to be thrown, you need to calltransException(true)
. See Throwing Exceptions.See Exceptions when Database Errors Occur for details.
HTTP Status Code and Exit Code of Uncaught Exceptions
If you expect Exception code as HTTP status code, the HTTP status code will be changed. In that case, you need to implement
HTTPExceptionInterface
in the Exception. See Specify HTTP Status Code in Your Exception.If you expect Exit code based on Exception code, the Exit code will be changed. In that case, you need to implement
HasExitCodeInterface
in the Exception. See Specify Exit Code in Your Exception.
redirect()->withInput() and Validation Errors
redirect()->withInput()
and Validation errors had an undocumented behavior.
If you redirect with withInput()
, CodeIgniter stores the validation errors
in the session, and you can get the errors in the redirected page from
a validation object before a new validation is run:
// In the controller
if (! $this->validate($rules)) {
return redirect()->back()->withInput();
}
// In the view of the redirected page
<?= service('Validation')->listErrors() ?>
This behavior was a bug and fixed in v4.3.0.
If you have code that depends on the bug, you need to change the code.
Use new Form helpers, validation_errors()
, validation_list_errors()
and validation_show_error()
to display Validation Errors,
instead of the Validation object.
Validation Changes
ValidationInterface
has been changed. Implemented classes should likewise add the methods and the parameters so as not to break LSP. See Validation Changes for details.The return value of
Validation::loadRuleGroup()
has been changednull
to[]
when the$group
is empty. Update the code if you depend on the behavior.
Time Fixes
Due to bug fixes, some methods in Time have changed from mutable behavior to immutable;
Time
now extendsDateTimeImmutable
. See ChangeLog for details.If you need the behavior of
Time
before the modification, a compatibleTimeLegacy
class has been added. Please replace allTime
withTimeLegacy
in your application code.But
TimeLegacy
is deprecated. So we recommend you update your code.
E.g.:
// Before
$time = Time::now();
// ...
if ($time instanceof DateTime) {
// ...
}
// After
$time = Time::now();
// ...
if ($time instanceof DateTimeInterface) {
// ...
}
// Before
$time1 = new Time('2022-10-31 12:00');
$time2 = $time1->modify('+1 day');
echo $time1; // 2022-11-01 12:00:00
echo $time2; // 2022-11-01 12:00:00
// After
$time1 = new Time('2022-10-31 12:00');
$time2 = $time1->modify('+1 day');
echo $time1; // 2022-10-31 12:00:00
echo $time2; // 2022-11-01 12:00:00
Capturing STDERR and STDOUT streams in Tests
The way error and output streams are captured has changed. Now instead of:
use CodeIgniter\Test\Filters\CITestStreamFilter;
protected function setUp(): void
{
CITestStreamFilter::$buffer = '';
$this->streamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter');
$this->streamFilter = stream_filter_append(STDERR, 'CITestStreamFilter');
}
protected function tearDown(): void
{
stream_filter_remove($this->streamFilter);
}
need to use:
use CodeIgniter\Test\Filters\CITestStreamFilter;
protected function setUp(): void
{
CITestStreamFilter::registration();
CITestStreamFilter::addOutputFilter();
CITestStreamFilter::addErrorFilter();
}
protected function tearDown(): void
{
CITestStreamFilter::removeOutputFilter();
CITestStreamFilter::removeErrorFilter();
}
Or use the trait CodeIgniter\Test\StreamFilterTrait
. See Testing CLI Output.
Interface Changes
Some interfaces has been fixed. See Interface Changes for details.
Foreign Key Data
The data structure returned by
BaseConnection::getForeignKeyData()
has been changed. You will need to adjust any code depending on this method to use the new structure.
Example: tableprefix_table_column1_column2_foreign
The data returned has the following structure:
/**
* @return array[
* {constraint_name} =>
* stdClass[
* 'constraint_name' => string,
* 'table_name' => string,
* 'column_name' => string[],
* 'foreign_table_name' => string,
* 'foreign_column_name' => string[],
* 'on_delete' => string,
* 'on_update' => string,
* 'match' => string
* ]
* ]
*/
Breaking Enhancements
Multiple Domain Support
If you set
Config\App::$allowedHostnames
, URL-related functions such asbase_url()
,current_url()
,site_url()
will return the URL with the hostname set inConfig\App::$allowedHostnames
if the current URL matches.
Database
The return type of
CodeIgniter\Database\Database::loadForge()
has been changed toForge
. Extending classes should likewise change the type.The return type of
CodeIgniter\Database\Database::loadUtils()
has been changed toBaseUtils
. Extending classes should likewise change the type.The second parameter
$index
ofBaseBuilder::updateBatch()
has changed to$constraints
. It now accepts types array, string, orRawSql
. Extending classes should likewise change types.The
$set
parameter ofBaseBuilder::insertBatch()
andBaseBuilder::updateBatch()
now accepts an object of a single row of data. Extending classes should likewise change the type.The third parameter
$index
ofBaseBuilder::_updateBatch()
has changed to$values
, and the parameter type has changed toarray
. Extending classes should likewise change the type.The
Model::update()
method now raises aDatabaseException
if it generates an SQL statement without a WHERE clause. If you need to update all records in a table, use Query Builder instead. E.g.,$model->builder()->update($data)
.
Honeypot and CSP
When CSP is enabled, id attribute id="hpc"
will be injected into the container tag
for the Honeypot field to hide the field. If the id is already used in your views, you need to change it
with Config\Honeypot::$containerId
.
And you can remove style="display:none"
in Config\Honeypot::$container
.
Others
Helper: Since void HTML elements (e.g.
<input>
) inhtml_helper
,form_helper
or common functions have been changed to be HTML5-compatible by default and you need to be compatible with XHTML, you must set the$html5
property in app/Config/DocTypes.php tofalse
.CLI: Since the launch of Spark Commands was extracted from
CodeIgniter\CodeIgniter
, there may be problems running these commands if theServices::codeigniter()
service has been overridden.
Project Files
Numerous files in the project space (root, app, public, writable) received updates. Due to these files being outside of the system scope they will not be changed without your intervention. There are some third-party CodeIgniter modules available to assist with merging changes to the project space: Explore on Packagist.
Content Changes
The following files received significant changes (including deprecations or visual adjustments) and it is recommended that you merge the updated versions with your application:
Config
- app/Config/App.php
The new property
$allowedHostnames
is added to set allowed hostnames in the site URL other than the hostname in the$baseURL
. See Multiple Domain Support.The property
$appTimezone
has been changed toUTC
to avoid being affected by daylight saving time.
- app/Config/Autoload.php
The new property
$helpers
is added to autoload helpers.
- app/Config/Database.php
$default['DBDebug']
and$test['DBDebug']
are changed totrue
by default. See Exceptions when Database Errors Occur.
- app/Config/DocTypes.php
The property
$html5
to determine whether to remove the solidus (/
) character for void HTML elements (e.g.<input>
) is added, and set totrue
by default for HTML5 compatibility.
- app/Config/Encryption.php
The new property
$rawData
,$encryptKeyInfo
, and$authKeyInfo
are added for CI3 Encryption compatibility. See Configuration to Maintain Compatibility with CI3.
- app/Config/Exceptions.php
Two additional public properties were added:
$logDeprecations
and$deprecationLogLevel
. See See Logging Deprecation Warnings for details.
- app/Config/Honeypot.php
The new property
$containerId
is added to set id attribute value for the container tag when CSP is enabled.The
input
tag in the property$template
value has been changed to HTML5 compatible.
- app/Config/Logger.php
The property
$threshold
has been changed to9
in other thanproduction
environment.
- app/Config/Modules.php
The new property
$composerPackages
is added to limit Composer package Auto-Discovery for better performance.
- app/Config/Routes.php
Due to the fact that the approach to running Spark Commands has changed, there is no longer a need to load the internal routes of the framework (
SYSTEMPATH . 'Config/Routes.php'
).
- app/Config/Security.php
Changed the value of the property
$redirect
tofalse
to prevent redirection when a CSRF check fails. This is to make it easier to recognize that it is a CSRF error.
- app/Config/Session.php
Added to handle session configuration.
- app/Config/Validation.php
The default Validation Rules have been changed to Strict Rules for better security. See Traditional and Strict Rules.
View Files
The following view files have been changed to HTML5 compatible tags. Also, error messages are now defined in the Errors language file.
app/Views/errors/html/error_404.php
app/Views/errors/html/error_exception.php
app/Views/errors/html/production.php
app/Views/welcome_message.php
All Changes
This is a list of all files in the project space that received changes;
many will be simple comments or formatting that have no effect on the runtime.
All atomic type properties in Config
classes have been typed:
app/Config/App.php
app/Config/Autoload.php
app/Config/CURLRequest.php
app/Config/Cache.php
app/Config/ContentSecurityPolicy.php
app/Config/Cookie.php
app/Config/Database.php
app/Config/DocTypes.php
app/Config/Email.php
app/Config/Encryption.php
app/Config/Exceptions.php
app/Config/Feature.php
app/Config/Filters.php
app/Config/Format.php
app/Config/Generators.php
app/Config/Honeypot.php
app/Config/Images.php
app/Config/Kint.php
app/Config/Logger.php
app/Config/Migrations.php
app/Config/Mimes.php
app/Config/Modules.php
app/Config/Pager.php
app/Config/Paths.php
app/Config/Routes.php
app/Config/Security.php
app/Config/Session.php
app/Config/Toolbar.php
app/Config/UserAgents.php
app/Config/Validation.php
app/Views/errors/html/error_404.php
app/Views/errors/html/error_exception.php
app/Views/errors/html/production.php
app/Views/welcome_message.php
composer.json
env
phpunit.xml.dist
spark


Projects in Web Enthusiasts Club are done on a semester basis where members are assigned projects mentors to work with on a particular project.
TEXT YOU WANT TO PLACE
Copyright© All Rights Reserved


AWS::Athena::NamedQuery
The
AWS::Athena::NamedQuery
resource creates an Amazon Athena query. For more information, see
CreateNamedQuery in the
Amazon Athena Documentation.
Topics
Syntax
To declare this entity in your AWS CloudFormation template, use the following syntax:
JSON
{ "Type" : "AWS::Athena::NamedQuery", "Properties" : { "Description" :
String
, "QueryString" :String
, "Database" :String
, "Name" :String
} }
YAML
Type: AWS::Athena::NamedQuery Properties: Description:
String
QueryString:String
Database:String
Name:String
Properties
For constraints, see NamedQuery in the Amazon Athena API Reference.
Description
-
A brief description of the query.
Required: No
Type: String
Update requires: No interruption
QueryString
-
The SQL query statements that comprise the query.
Required: Yes
Type: String
Update requires: Replacement
Database
-
The database to which the query belongs.
Required: Yes
Type: String
Update requires: No interruption
Name
-
The plain-language name of the query.
Required: No
Type: String
Update requires: Replacement
Return Values
Ref
When the logical ID of this resource is provided to the Ref
intrinsic
function, Ref
returns the resource name.
For more information about using the
Ref
function, see
Ref.
Examples
The following example creates a named query.
JSON
{ "Resources": { "AthenaNamedQuery": { "Type": "AWS::Athena::NamedQuery", "Properties": { "Database": "swfnetadata", "Description": "A query that selects all aggregated data", "Name": "MostExpensiveWorkflow", "QueryString": "SELECT workflowname, AVG(activitytaskstarted) AS AverageWorkflow FROM swfmetadata WHERE year='17' AND GROUP BY workflowname ORDER BY AverageWorkflow DESC LIMIT 10" } } } }
YAML
Resources: AthenaNamedQuery: Type: AWS::Athena::NamedQuery Properties: Database: "swfnetadata" Description: "A query that selects all aggregated data" Name: "MostExpensiveWorkflow" QueryString: > SELECT workflowname, AVG(activitytaskstarted) AS AverageWorkflow FROM swfmetadata WHERE year='17' AND GROUP BY workflowname ORDER BY AverageWorkflow DESC LIMIT 10


Use this area to configure the JVM settings. You can choose from the following options:
- JRE:
select which JDK you want to use in your project. By default, IntelliJ IDEA uses the project's JDK.
- Maximum heap size, MB:
use this field to specify the maximum heap size available to the process that launches the compiler. The default
768 Mb is suitable for most of the purposes.
- VM parameters:
use this field to type the string to be passed to the VM when IntelliJ IDEA launches the compiler. If you need
more room to type, click the Add button to open the VM parameters dialog where the
text entry area is larger.


What are we talking about today
- Choices: Design, Patterns and what nots
- Dive into Mirror API
- Making Mirror do more with App Engine
- The GDK!
Glass: not your average screen
- You should be designing for Glass
- Direct ports = not a great choice
Don't get in the way
Supplement the users life, don't take it over
Stay contextual and relevent
Info at the right place, right time
Avoid unexpected actions
Be aware otherwise your user will turn you off
Build for people
The robots are okay, they're not interested in Glass
Design has never been more important
We need some building blocks
- UI elements: static cards, live cards, immersion
- Invocation: "OK Glass", voice menu, touch menu, contextual voice
The actual "doing" part
- Focus on the action, not the agent
- Cut down the time between intent and action
- Easy to say
Understanding the patterns
- Periodic
- Ongoing task
- Immersion
Periodic
- Insert timeline cards into the timeline without invocation
- Good case for notifications from an remote service
- Most common case for Mirror API
From Google's Glass Design Docs - Periodic
Ongoing task
- Long running live cards
- User can leave/return as needed
- Only the GDK
From Google's Glass Design Docs - Ongoing
Immersion
- Take over the timeline experience
- Users leave by swiping down
- Only the GDK (Android activities)
From Google's Glass Design Docs - Ongoing
Tools that rock: Glassware Flow Designer
Easiser way to design your app flow
What if my app doesn't fit the pattern?
- Consider your end goal for your application
- GDK: real-time, offline, access to the hardware
- Mirror API: platform indepent, common infratructure, use built-in functionality
- Hybrid approaches: Mirror card adds menu item that invokes intent to start
There is no one answer
- OH: "Java is the only way to write for Glass".
- OH: "Mirror API isn't powerful".
Both statements above = NOT TRUE
Decide for yourself, don't let others decide for you.
Only you know your goal and spec.
Jumping in to the Mirror API
- You're platform independent! You can choose your language and platform.
- You can make raw requests or for simplier access utilize the Google APIs Client Libs for your language
Mirror API Setup
OAuth
- Create your Client ID for you app under APIs & auth > Credentials
- You can set the Redirect URI's to wherever your project will be dev/deployed
Great!
- Doing the sign in - Google+ Sign In button
- Button implements JavaScript hooks in client side that call our server
connectServer: function() {
console.log(this.authResult.code);
$.ajax({
type: 'POST',
url: "//" + window.location.host + '/connect?state=' + this.state,
contentType: 'application/octet-stream; charset=utf-8',
success: function(result) {
console.log(result);
},
processData: false,
data: this.authResult.code
});
}
Server handles the tokens
try:
user_credentials = _oauth_flow().step2_exchange(request.data)
except FlowExchangeError:
response = make_response(json.dumps('Failed to upgrade the authorization code.'), 401)
response.headers['Content-Type'] = 'application/json'
return response
# Set our session
session['credentials'] = user_credentials
session['user_id'] = user_credentials.id_token['sub']
Once we have tokens we get into Mirror API
- Timeline
- Menu
- Subscriptions
I thought the Mirror API had more?
Yes, it does. But let's dive into the basics first.
The timeline and you: Insert a card
- Timeline consists of cards or various types and styles
- Inserting a welcome card with a user auth'ed Mirror service
mirror_service = _authorized_mirror_service(user_credentials)
mirror_service.timeline().insert(body={
'notification': {'level': 'DEFAULT'},
'text': 'Welcome to Glass! This is my very first timeline card insert.'
).execute()
Timeline cards have lots of options
mirror_service.timeline().insert(body={
'notification': {'level': 'DEFAULT'},
'html': 'Welcome to
Glass! This is my very first timeline card insert.
'
).execute()
Timeline cards: more options!
mirror_service.timeline().insert(body={
'notification': {'level': 'DEFAULT'},
'location': {
'latitude': 37.7692,
'longitude': 120.8569,
"displayName": "Season 6 Dreams Meetup",
"address": "Group Study Room F, Greendale Community College"
}
).execute()
Timeline cards: more options!
mirror_service.timeline().insert(body={
'notification': {'level': 'DEFAULT'},
'text': 'Video time!',
'payload': 'http://mymystical.url/video.mp4',
'menuItems': [{'action': 'PLAY_VIDEO'}]
).execute()
Was that a Menu Item in that last example?
- Menu options give uses the ability to act on a timeline card
- Lots of built in options: REPLY, DELETE, READ_ALOUD, NAVIGATE, OPEN_URI and many more
- Can set custom menu options with CUSTOM that callback to a subscription
A basic MenuItem example
mirror_service.timeline().insert(body={
'notification': {'level': 'DEFAULT'},
'text': 'Timeline card with a couple menu options.',
'menuItems': [
{'action': 'CUSTOM', 'id': 'my-id', 'values': [ { 'displayName': "My Menu Item" } ] },
{'action': 'DELETE'}]
}).execute()
From Menu Actions to Subscriptions
- When a user acts on a menu item, we can listen for those action via a Subscription
- We set a callbackUrl that allows our application to receive a POST notification from the Mirror API.
A sample subscription for timeline
mirror_service.subscriptions().insert(body={
'collection': 'timeline',
'userToken': user_id,
'verifyToken': 'something_only_you_know',
'callbackUrl': 'https://my-project-id.appspot.com/myGlassCallback',
'operation': ['UPDATE']
}).execute()
Breaking it down
- collection: timeline or location
- userToken: something to id your user
- verifyToken: something only you know to verify notification
- operation: UPDATE, INSERT, DELETE
Then there's callbackUrl
- In dev, you can use the SSL proxy
- In prod, you have to use an SSL endpoint
- The problem: no one wants to deploy remote even with a SSL proxy
- Solution: ngrok
ngork for the win
- Allows you to create a reverse tunnel
- Gives you an nice web panel to see incoming requests
- Gives you an SSL endpoint
~ $ ngrok -authtoken YOURTOKEN TARGETPORT
~ $ ngrok 8080
Ngrok in action
Ngrok viewing Mirror callback data
The code change is tiny
mirror_service.subscriptions().insert(body={
'collection': 'timeline',
'userToken': user_id,
'verifyToken': 'something_only_you_know',
'callbackUrl': 'https://RANDOM.ngrok.com/myGlassCallback',
'operation': ['UPDATE']
}).execute()
That's the crash course
I must do more!
Making Mirror shine
- Mirror has a lot of power, we can send all kinds of sticky, relevent data
- But how do we do that?
- To the samples!
Request: How do you send a card daily?
- Daily sort of jobs generally require cron
- If we're on our own boxes, we could schedule a script in crontab
- If we're on App Engine, we can use App Engines Cron
A simple App Engine Cron job
@glassdailycard.route('/dailyjob')
def dailyjob():
today = datetime.date.today()
query_get_todays_card = CronCards.query(CronCards.date == today)
result_get_todays_card = query_get_todays_card.get()
# Do we have a card to send today?
if result_get_todays_card is not None:
timelinecard_body = {
'notification': {'level': 'DEFAULT'},
'text': result_get_todays_card.card,
'menuItems': [{'action': 'DELETE'}]
}
query_get_users = UserProperties.query()
for user in query_get_users.fetch():
user_credentials = _credentials_for_user(user.key.id()).get()
_authorized_mirror_service(user_credentials).timeline().insert(body=timelinecard_body).execute()
response = make_response("{}", 200)
response.headers['Content-Type'] = 'application/json'
return response
Request: How do you send a card random in future?
- Generally requires a work queue of some type
- If we're on our own boxes, we could use beanstalkd
- If we're on App Engine, we can use App Engines Push Task Queue
A simple App Engine Task Queue
@glasstaskfuture.route('/glassCallback', methods=['POST'])
def glassCallback():
"""Our callback when the use selects out CUSTOM menu option on the start card."""
notification = request.data
if random.choice([True, False]):
# we're going to send it along to the task queue
google.appengine.api.taskqueue.add(
url='/taskHandler',
payload=notification,
countdown=60)
else:
now = datetime.datetime.now()
future = now + datetime.timedelta(0,60)
google.appengine.api.taskqueue.add(
url='/taskHandler',
payload=notification,
eta=future)
# It's cool Mirror API
#
# Remember, we must return 200 as quickly as we can!
#
return('OK')
glass-task-future
- Simple starter project that shows basics of using App Engine Task Queue with a subscription callback
- Uses Google+ Sign-In
- Repo: justinribeiro/glass-task-future
What about the GDK?
- Like developing for Android
- For when you need more access to Glass hardware
UPDATE: Nice things that happened this week
- Whoo hooo, notification sync with WearableExtender!
- Includes voice reply, pages, stacks...the works
- I'll talk more about this during Wear talk
The Live card
- When you need that long running context
- "OK Glass"...voice trigger
- Low-Freq updates to Android views
- High-Freq updates can use graphics framework (OpenGL)
Example: low frequency mqtt live card
Because we need to talk to Arduino people.
Of course that code is on Github
Push it: Glass to the web
- Lots of ways to do this
- I heart the MQTT - let's use a broker!
- Glass > Broker > WebSocket > Your Browser
A Glass like View
- UI Widgets: CardBuilder and CardScrollAdapter
- CardBuilder.Layout allows us to create Glass-like cards
- We can convert to an Android View or RemoteViews
- CardScrollAdapter gives us Glass-like timeline
Sensors oh my!
- Host of sensors for use: TYPE_ACCELEROMETER, TYPE_LIGHT, TYPE_ROTATION_VECTOR
- Touch Gesture > D-pad key events
- Location can be tricky: don't use LocationManager.getBestProvider()
Speaking of location...
LocationManager locationManager; // initialized elsewhere
// This example requests fine accuracy and requires altitude, but
// these criteria could be whatever you want.
Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_FINE);
criteria.setAltitudeRequired(true);
List providers = locationManager.getProviders(
criteria, true /* enabledOnly */);
for (String provider : providers) {
locationManager.requestLocationUpdates(provider, minTime,
minDistance, listener);
}
With great GDK power, comes overheating
- If you push the hardware too hard you will get overheating messages
- Offload if you can, your battery is limited
- Just because you can, doesn't mean you should
Further reading / resources


WEB POS
Dashboard
(current)
Manage Customer
Manage Item
Place Orders
Search Orders
Manage Item
Item details are as follows.
Image
Code
Name
Unit Price
Quanity
I001
Wireless Keyboard
1500
50


BBLog DEV <IP_ADDRESS> out now - Better Battlelog Forums #127082
Sitemap
15. September 2014 09:21
Topicstarter
BrainFooLong
Administrator
Another DEV update with some new stuff in it.
* optimized plugin/theme editor
* optimized plugin gallery
* added forums auto collapse
* added bblog teamspeak icon


HELLO! WELCOME TO MY FAMILY WEBSITE.THIS IS Brother/Sister Page.
Home
Pranets&Sons
Brother/Sister
Family Image
CONTENT ME