/**
  * add requirements for frontend editing only when logged in
  * @todo Use TinyMCEs Compressor 4.0.2 PHP
  */
 public function onBeforeInit()
 {
     $canEdit = FrontendEditing::ShowAdmin();
     $editingEnabled = FrontendEditing::editingEnabled();
     $minExt = Director::isDev() ? "" : ".min";
     if ($canEdit) {
         // Enable front-end fly-out menu
         //
         //Flexslider imports easing, which breaks?
         Requirements::block('flexslider/javascript/jquery.easing.1.3.js');
         Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
         Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-ui/jquery-ui.js');
         Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/ssui.core.js');
         Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js');
         Requirements::javascriptTemplate(FRONTEND_ADMIN_DIR . '/javascript/dist/FrontEndAdminTemplate' . $minExt . '.js', $this->getConfig($this->owner->data()));
         Requirements::css(FRAMEWORK_DIR . '/thirdparty/jquery-ui-themes/smoothness/jquery-ui.css');
         Requirements::javascript(FRONTEND_ADMIN_DIR . '/javascript/dist/FrontEndAdmin' . $minExt . '.js');
         Requirements::css(FRONTEND_ADMIN_DIR . '/css/frontend-admin' . $minExt . '.css');
         $this->owner->getResponse()->addHeader("X-DynamicCache-OptOut", true);
     }
     if ($canEdit && $editingEnabled) {
         // Disable mode pagespeed while editing
         $this->owner->getResponse()->addHeader("PageSpeed", "off");
         // Disable HTTP cache while editing
         HTTP::set_cache_age(0);
         // Enable TinyMCE when editing has been enabled
         Requirements::javascript(FRONTEND_ADMIN_DIR . '/bower_components/tinymce/jquery.tinymce.min.js');
         Requirements::javascript(FRONTEND_ADMIN_DIR . '/javascript/dist/FrontEndEditor' . $minExt . '.js');
         Requirements::css(FRONTEND_ADMIN_DIR . '/css/frontend-editor' . $minExt . '.css');
     }
 }
Пример #2
0
 public function testAddCacheHeaders()
 {
     $body = "<html><head></head><body><h1>Mysite</h1></body></html>";
     $response = new SS_HTTPResponse($body, 200);
     $this->assertEmpty($response->getHeader('Cache-Control'));
     HTTP::set_cache_age(30);
     HTTP::add_cache_headers($response);
     $this->assertNotEmpty($response->getHeader('Cache-Control'));
     // Ensure max-age is zero for development.
     Config::inst()->update('Director', 'environment_type', 'dev');
     $response = new SS_HTTPResponse($body, 200);
     HTTP::add_cache_headers($response);
     $this->assertContains('max-age=0', $response->getHeader('Cache-Control'));
     // Ensure max-age setting is respected in production.
     Config::inst()->update('Director', 'environment_type', 'live');
     $response = new SS_HTTPResponse($body, 200);
     HTTP::add_cache_headers($response);
     $this->assertContains('max-age=30', explode(', ', $response->getHeader('Cache-Control')));
     $this->assertNotContains('max-age=0', $response->getHeader('Cache-Control'));
     // Still "live": Ensure header's aren't overridden if already set (using purposefully different values).
     $headers = array('Vary' => '*', 'Pragma' => 'no-cache', 'Cache-Control' => 'max-age=0, no-cache, no-store');
     $response = new SS_HTTPResponse($body, 200);
     foreach ($headers as $name => $value) {
         $response->addHeader($name, $value);
     }
     HTTP::add_cache_headers($response);
     foreach ($headers as $name => $value) {
         $this->assertEquals($value, $response->getHeader($name));
     }
 }
 public function getJobStatus()
 {
     // Set headers
     HTTP::set_cache_age(0);
     HTTP::add_cache_headers($this->response);
     $this->response->addHeader('Content-Type', 'application/json')->addHeader('Content-Encoding', 'UTF-8')->addHeader('X-Content-Type-Options', 'nosniff');
     // Format status
     $track = BrokenExternalPageTrackStatus::get_latest();
     if ($track) {
         return json_encode(array('TrackID' => $track->ID, 'Status' => $track->Status, 'Completed' => $track->getCompletedPages(), 'Total' => $track->getTotalPages()));
     }
 }
 public function run($request)
 {
     HTTP::set_cache_age(0);
     increase_time_limit_to();
     // This can be a time consuming task
     $conn = DB::getConn();
     $classes = ClassInfo::subclassesFor('DataObject');
     $dbTables = $conn->tableList();
     $go = $request->getVar('go');
     if (!$go) {
         echo 'Set ?go=1 to really delete the tables';
         echo '<hr/>';
     }
     //make all lowercase
     $dbTablesLc = array_map('strtolower', $dbTables);
     $dbTablesMap = array();
     foreach ($dbTables as $k => $v) {
         $dbTablesMap[strtolower($v)] = $v;
     }
     foreach ($classes as $class) {
         if (ClassInfo::hasTable($class)) {
             $lcClass = strtolower($class);
             self::removeFromArray($lcClass, $dbTablesLc);
             //page modules
             self::removeFromArray($lcClass . '_live', $dbTablesLc);
             self::removeFromArray($lcClass . '_versions', $dbTablesLc);
             //relations
             $hasMany = Config::inst()->get($class, 'has_many');
             $manyMany = Config::inst()->get($class, 'many_many');
             if (!empty($hasMany)) {
                 foreach ($hasMany as $rel => $obj) {
                     self::removeFromArray($lcClass . '_' . strtolower($rel), $dbTablesLc);
                 }
             }
             if (!empty($manyMany)) {
                 foreach ($manyMany as $rel => $obj) {
                     self::removeFromArray($lcClass . '_' . strtolower($rel), $dbTablesLc);
                 }
             }
         }
     }
     //at this point, we should only have orphans table in dbTables var
     foreach ($dbTablesLc as $i => $lcTable) {
         $table = $dbTablesMap[$lcTable];
         if ($go) {
             DB::query('DROP TABLE `' . $table . '`');
             DB::alteration_message("Dropped {$table}", 'obsolete');
         } else {
             DB::alteration_message("Would drop {$table}", 'obsolete');
         }
     }
 }
Пример #5
0
 public function testAddCacheHeaders()
 {
     $body = "<html><head></head><body><h1>Mysite</h1></body></html>";
     $response = new SS_HTTPResponse($body, 200);
     $this->assertEmpty($response->getHeader('Cache-Control'));
     HTTP::set_cache_age(30);
     HTTP::add_cache_headers($response);
     $this->assertNotEmpty($response->getHeader('Cache-Control'));
     Config::inst()->update('Director', 'environment_type', 'dev');
     HTTP::add_cache_headers($response);
     $this->assertContains('max-age=0', $response->getHeader('Cache-Control'));
     Config::inst()->update('Director', 'environment_type', 'live');
     HTTP::add_cache_headers($response);
     $this->assertContains('max-age=30', explode(', ', $response->getHeader('Cache-Control')));
     $this->assertNotContains('max-age=0', $response->getHeader('Cache-Control'));
 }
Пример #6
0
 public function testConfigVary()
 {
     $body = "<html><head></head><body><h1>Mysite</h1></body></html>";
     $response = new SS_HTTPResponse($body, 200);
     Config::inst()->update('Director', 'environment_type', 'live');
     HTTP::set_cache_age(30);
     HTTP::add_cache_headers($response);
     $v = $response->getHeader('Vary');
     $this->assertNotEmpty($v);
     $this->assertContains("Cookie", $v);
     $this->assertContains("X-Forwarded-Protocol", $v);
     $this->assertContains("User-Agent", $v);
     $this->assertContains("Accept", $v);
     Config::inst()->update('HTTP', 'vary', '');
     $response = new SS_HTTPResponse($body, 200);
     HTTP::add_cache_headers($response);
     $v = $response->getHeader('Vary');
     $this->assertEmpty($v);
 }
 public function run($request)
 {
     HTTP::set_cache_age(0);
     set_time_limit(0);
     $classes = SubsiteDataObjectMany::extendedClasses();
     foreach ($classes as $cl) {
         $s = singleton($cl);
         $rec = $cl::get();
         foreach ($rec as $r) {
             $oldList = $r->SubsiteList;
             $list = $r->buildSubsiteList();
             if ($list != $oldList) {
                 $qry = "UPDATE {$cl} SET SubsiteList = '{$list}' WHERE ID = {$r->ID}";
                 DB::query($qry);
                 echo "{$qry}<br/>";
             }
         }
     }
     echo 'All done!';
 }
 public function run($request)
 {
     HTTP::set_cache_age(0);
     increase_time_limit_to();
     // This can be a time consuming task
     $classes = ClassInfo::dataClassesFor('DataObject');
     $conn = DB::getConn();
     $go = $request->getVar('go');
     if (!$go) {
         echo 'Set ?go=1 to really delete the fields';
         echo '<hr/>';
     }
     foreach ($classes as $class) {
         $hasTable = ClassInfo::hasTable($class);
         if (!$hasTable) {
             continue;
         }
         $toDrop = array();
         $fields = $class::database_fields($class);
         $list = $conn->fieldList($class);
         foreach ($list as $fieldName => $type) {
             if ($fieldName == 'ID') {
                 continue;
             }
             if (!isset($fields[$fieldName])) {
                 $toDrop[] = $fieldName;
             }
         }
         if (empty($toDrop)) {
             continue;
         }
         if ($go) {
             $this->dropColumns($class, $toDrop);
             DB::alteration_message("Dropped " . implode(',', $toDrop) . " for {$class}", "obsolete");
         } else {
             DB::alteration_message("Would drop " . implode(',', $toDrop) . " for {$class}", "obsolete");
         }
     }
 }
Пример #9
0
 public static function getRedirectBackUrl()
 {
     $url = null;
     // Don't cache the redirect back ever
     HTTP::set_cache_age(0);
     // In edge-cases, this will be called outside of a handleRequest() context; in that case,
     // redirect to the homepage - don't break into the global state at this stage because we'll
     // be calling from a test context or something else where the global state is inappropraite
     if ($request = Controller::curr()->getRequest()) {
         if ($request->requestVar('BackURL')) {
             $url = $request->requestVar('BackURL');
         } else {
             if ($request->isAjax() && $request->getHeader('X-Backurl')) {
                 $url = $request->getHeader('X-Backurl');
             }
         }
     }
     $url = self::cleanBackUrl($url);
     if (strpos($url, '/Security/login') !== false) {
         $url = Director::baseURL();
     }
     return $url;
 }
Пример #10
0
 /**
  * Get the RSS feed
  *
  * This method will output the RSS feed with the last 50 posts to the
  * browser.
  */
 function rss()
 {
     HTTP::set_cache_age(3600);
     // cache for one hour
     $threadID = null;
     $forumID = null;
     // optionally allow filtering of the forum posts by the url in the format
     // rss/thread/$ID or rss/forum/$ID
     if (isset($this->urlParams['ID']) && ($action = $this->urlParams['ID'])) {
         if (isset($this->urlParams['OtherID']) && ($id = $this->urlParams['OtherID'])) {
             switch ($action) {
                 case 'forum':
                     $forumID = (int) $id;
                     break;
                 case 'thread':
                     $threadID = (int) $id;
             }
         } else {
             // fallback is that it is the ID of a forum like it was in
             // previous versions
             $forumID = (int) $action;
         }
     }
     $data = array('last_created' => null, 'last_id' => null);
     if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && !isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
         // just to get the version data..
         $this->getNewPostsAvailable(null, null, $forumID, $threadID, &$data);
         // No information provided by the client, just return the last posts
         $rss = new RSSFeed($this->getRecentPosts(50, $forumID, $threadID), $this->Link() . 'rss', sprintf(_t('Forum.RSSFORUMPOSTSTO'), $this->Title), "", "Title", "RSSContent", "RSSAuthor", $data['last_created'], $data['last_id']);
         $rss->outputToBrowser();
     } else {
         // Return only new posts, check the request headers!
         $since = null;
         $etag = null;
         if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
             // Split the If-Modified-Since (Netscape < v6 gets this wrong)
             $since = explode(';', $_SERVER['HTTP_IF_MODIFIED_SINCE']);
             // Turn the client request If-Modified-Since into a timestamp
             $since = @strtotime($since[0]);
             if (!$since) {
                 $since = null;
             }
         }
         if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && is_numeric($_SERVER['HTTP_IF_NONE_MATCH'])) {
             $etag = (int) $_SERVER['HTTP_IF_NONE_MATCH'];
         }
         if ($this->getNewPostsAvailable($since, $etag, $forumID, $threadID, $data)) {
             HTTP::register_modification_timestamp($data['last_created']);
             $rss = new RSSFeed($this->getRecentPosts(50, $forumID, $threadID, $etag), $this->Link() . 'rss', sprintf(_t('Forum.RSSFORUMPOSTSTO'), $this->Title), "", "Title", "RSSContent", "RSSAuthor", $data['last_created'], $data['last_id']);
             $rss->outputToBrowser();
         } else {
             if ($data['last_created']) {
                 HTTP::register_modification_timestamp($data['last_created']);
             }
             if ($data['last_id']) {
                 HTTP::register_etag($data['last_id']);
             }
             // There are no new posts, just output an "304 Not Modified" message
             HTTP::add_cache_headers();
             header('HTTP/1.1 304 Not Modified');
         }
     }
     exit;
 }
Пример #11
0
 function init()
 {
     parent::init();
     HTTP::set_cache_age(0);
 }
 /**
  * Returns the shopping cart.
  * @todo Does HTTP::set_cache_age() still need to be set here?
  * 
  * @return Order
  */
 function Cart()
 {
     HTTP::set_cache_age(0);
     return ShoppingCart::current_order();
 }
Пример #13
0
 /**
  * Redirect back. Uses either the HTTP_REFERER or a manually set request-variable called "BackURL".
  * This variable is needed in scenarios where not HTTP-Referer is sent (
  * e.g when calling a page by location.href in IE).
  * If none of the two variables is available, it will redirect to the base
  * URL (see {@link Director::baseURL()}).
  * @uses redirect()
  */
 public function redirectBack()
 {
     // Don't cache the redirect back ever
     HTTP::set_cache_age(0);
     $url = null;
     // In edge-cases, this will be called outside of a handleRequest() context; in that case,
     // redirect to the homepage - don't break into the global state at this stage because we'll
     // be calling from a test context or something else where the global state is inappropraite
     if ($this->request) {
         if ($this->request->requestVar('BackURL')) {
             $url = $this->request->requestVar('BackURL');
         } else {
             if ($this->request->isAjax() && $this->request->getHeader('X-Backurl')) {
                 $url = $this->request->getHeader('X-Backurl');
             } else {
                 if ($this->request->getHeader('Referer')) {
                     $url = $this->request->getHeader('Referer');
                 }
             }
         }
     }
     if (!$url) {
         $url = Director::baseURL();
     }
     // absolute redirection URLs not located on this site may cause phishing
     if (Director::is_site_url($url)) {
         $url = Director::absoluteURL($url, true);
         return $this->redirect($url);
     } else {
         return false;
     }
 }
Пример #14
0
 /**
  * Return the attributes of the form tag - used by the templates.
  * 
  * @return string The attribute string
  */
 function FormAttributes()
 {
     $attributes = array();
     // Forms shouldn't be cached, cos their error messages won't be shown
     HTTP::set_cache_age(0);
     // workaround to include javascript validation
     if ($this->validator && !$this->jsValidationIncluded) {
         $this->validator->includeJavascriptValidation();
     }
     // compile attributes
     $attributes['id'] = $this->FormName();
     $attributes['action'] = $this->FormAction();
     $attributes['method'] = $this->FormMethod();
     $attributes['enctype'] = $this->FormEncType();
     if ($this->target) {
         $attributes['target'] = $this->target;
     }
     if ($this->extraClass()) {
         $attributes['class'] = $this->extraClass();
     }
     if ($this->validator && $this->validator->getErrors()) {
         if (!isset($attributes['class'])) {
             $attributes['class'] = '';
         }
         $attributes['class'] .= ' validationerror';
     }
     // implode attributes into string
     $preparedAttributes = '';
     foreach ($attributes as $k => $v) {
         // Note: as indicated by the $k == value item here; the decisions over what to include in the attributes can sometimes get finicky
         if (!empty($v) || $v === '0' || $k == 'value') {
             $preparedAttributes .= " {$k}=\"" . Convert::raw2att($v) . "\"";
         }
     }
     return $preparedAttributes;
 }
<?php

global $project;
$project = 'mysite';
// use the _ss_environment.php file for configuration
require_once 'conf/ConfigureFromEnv.php';
// set default language
i18n::set_locale('en_US');
define('PROJECT_THIRDPARTY_DIR', project() . '/thirdparty');
define('PROJECT_THIRDPARTY_PATH', BASE_PATH . '/' . PROJECT_THIRDPARTY_DIR);
if (SS_IS_TEST_ENV) {
    BasicAuth::protect_entire_site(true);
}
if (Director::isLive()) {
    if (strpos(Director::absoluteBaseURL(), 'silverstripe-europe.org') !== false || strpos(Director::absoluteBaseURL(), 'www') !== false) {
        $response = new SS_HTTPResponse();
        $response->redirect('https://stripecon.eu', 301);
        HTTP::add_cache_headers($response);
        $response->output();
        die;
    }
    // we are in live mode, send errors per email, set cache and force WWW
    HTTP::set_cache_age(3600);
    // HTTP Header for CloudFlare Caching
    SS_Cache::set_cache_lifetime('any', 10800);
    // Serverside cache to 3 hours.
    SS_Log::add_writer(new SS_LogEmailWriter('*****@*****.**'), SS_Log::ERR);
}
Config::inst()->update('HtmlEditorField', 'use_gzip', false);
Пример #16
0
 /**
  * Return the attributes of the form tag - used by the templates.
  * 
  * @param Array Custom attributes to process. Falls back to {@link getAttributes()}.
  * If at least one argument is passed as a string, all arguments act as excludes by name.
  * @return String HTML attributes, ready for insertion into an HTML tag
  */
 public function getAttributesHTML($attrs = null)
 {
     $exclude = is_string($attrs) ? func_get_args() : null;
     if (!$attrs || is_string($attrs)) {
         $attrs = $this->getAttributes();
     }
     // Figure out if we can cache this form
     // - forms with validation shouldn't be cached, cos their error messages won't be shown
     // - forms with security tokens shouldn't be cached because security tokens expire
     $needsCacheDisabled = false;
     if ($this->getSecurityToken()->isEnabled()) {
         $needsCacheDisabled = true;
     }
     if ($this->FormMethod() != 'get') {
         $needsCacheDisabled = true;
     }
     if (!$this->validator instanceof RequiredFields || count($this->validator->getRequired())) {
         $needsCacheDisabled = true;
     }
     // If we need to disable cache, do it
     if ($needsCacheDisabled) {
         HTTP::set_cache_age(0);
     }
     $attrs = $this->getAttributes();
     // Remove empty
     $attrs = array_filter((array) $attrs, create_function('$v', 'return ($v || $v === 0);'));
     // Remove excluded
     if ($exclude) {
         $attrs = array_diff_key($attrs, array_flip($exclude));
     }
     // Create markkup
     $parts = array();
     foreach ($attrs as $name => $value) {
         $parts[] = $value === true ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\"";
     }
     return implode(' ', $parts);
 }
 /**
  *
  * @standard SS method
  */
 public function init()
 {
     HTTP::set_cache_age(0);
     parent::init();
     // find the current order if any
     $orderID = 0;
     $overrideCanView = false;
     //WE HAVE THIS FOR SUBMITTING FORMS!
     if (isset($_REQUEST['OrderID'])) {
         $orderID = intval($_REQUEST['OrderID']);
         if ($orderID) {
             $this->currentOrder = Order::get()->byID($orderID);
         }
     } elseif ($this->request && $this->request->param('ID') && $this->request->param('Action')) {
         //we can not do intval here!
         $id = $this->request->param('ID');
         $action = $this->request->param('Action');
         $otherID = intval($this->request->param("OtherID"));
         //the code below is for submitted orders, but we still put it here so
         //we can do all the retrieval options in once.
         if ($action == "retrieveorder" && $id && $otherID) {
             $sessionID = Convert::raw2sql($id);
             $retrievedOrder = Order::get()->Filter(array("SessionID" => $sessionID, "ID" => $otherID))->First();
             $this->currentOrder = $retrievedOrder;
             $overrideCanView = true;
         } elseif (intval($id) && in_array($action, $this->stat("allowed_actions"))) {
             $this->currentOrder = Order::get()->byID(intval($id));
         }
     }
     if (!$this->currentOrder) {
         $this->currentOrder = ShoppingCart::current_order();
         if ($this->currentOrder) {
             if ($this->currentOrder->IsSubmitted()) {
                 $overrideCanView = true;
             }
         }
     }
     //redirect if we are viewing the order with the wrong page!
     if ($this->currentOrder) {
         //IMPORTANT SECURITY QUESTION!
         if ($this->currentOrder->canView() || $overrideCanView) {
             if ($this->currentOrder->IsSubmitted() && $this->onlyShowUnsubmittedOrders()) {
                 $this->redirect($this->currentOrder->Link());
             } elseif (!$this->currentOrder->IsSubmitted() && $this->onlyShowSubmittedOrders()) {
                 $this->redirect($this->currentOrder->Link());
             }
         } else {
             if (!$this->LoginToOrderLinkLabel) {
                 $this->LoginToOrderLinkLabel = _t('CartPage.LOGINFIRST', 'You will need to log in before you can access the requested order order. ');
             }
             $messages = array('default' => '<p class="message good">' . $this->LoginToOrderLinkLabel . '</p>', 'logInAgain' => _t('CartPage.LOGINAGAIN', 'You have been logged out. If you would like to log in again, please do so below.'));
             Security::permissionFailure($this, $messages);
             return false;
         }
         if (!$this->currentOrder->IsSubmitted()) {
             //we always want to make sure the order is up-to-date.
             $this->currentOrder->init($force = false);
             $this->currentOrder->calculateOrderAttributes($force = true);
             $this->currentOrder->calculateOrderAttributes($force = true);
         }
     } else {
         $this->message = _t('CartPage.ORDERNOTFOUND', 'Order can not be found.');
     }
 }
Пример #18
0
 /**
  * Return the attributes of the form tag - used by the templates
  * @return string The attribute string
  */
 function FormAttributes()
 {
     // Forms shouldn't be cached, cos their error messages won't be shown
     HTTP::set_cache_age(0);
     if ($this->validator) {
         $this->validator->includeJavascriptValidation();
     }
     if ($this->target) {
         $target = " target=\"" . $this->target . "\"";
     } else {
         $target = "";
     }
     return "id=\"" . $this->FormName() . "\" action=\"" . $this->FormAction() . "\" method=\"" . $this->FormMethod() . "\" enctype=\"" . $this->FormEncType() . "\"{$target}";
 }
Пример #19
0
 /**
  * Returns the shopping cart.
  * @todo Does HTTP::set_cache_age() still need to be set here?
  *
  * @return Order
  */
 function getCart()
 {
     if (!self::$global_allow_purchase) {
         return false;
     }
     HTTP::set_cache_age(0);
     return ShoppingCart::current_order();
 }
 /**
  * Ensures the response has the correct headers
  */
 protected function setHeaders()
 {
     // Set headers
     HTTP::set_cache_age(0);
     HTTP::add_cache_headers($this->response);
     $this->response->addHeader('Content-Type', 'application/json')->addHeader('Content-Encoding', 'UTF-8')->addHeader('X-Content-Type-Options', 'nosniff');
 }
Пример #21
0
	/**
	 * Return the attributes of the form tag - used by the templates.
	 * 
	 * @param Array Custom attributes to process. Falls back to {@link getAttributes()}.
	 * If at least one argument is passed as a string, all arguments act as excludes by name.
	 * @return String HTML attributes, ready for insertion into an HTML tag
	 */
	public function getAttributesHTML($attrs = null) {
		$exclude = (is_string($attrs)) ? func_get_args() : null;

		if(!$attrs || is_string($attrs)) $attrs = $this->getAttributes();

		// Forms shouldn't be cached, cos their error messages won't be shown
		HTTP::set_cache_age(0);

		$attrs = $this->getAttributes();

		// Remove empty
		$attrs = array_filter((array)$attrs, create_function('$v', 'return ($v || $v === 0);')); 
		
		// Remove excluded
		if($exclude) $attrs = array_diff_key($attrs, array_flip($exclude));

		// Create markkup
		$parts = array();
		foreach($attrs as $name => $value) {
			$parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\"";
		}

		return implode(' ', $parts);
	}