Merge remote-tracking branch 'origin/3.1'

Conflicts:
	api/RSSFeed.php
This commit is contained in:
Ingo Schommer 2013-02-18 14:18:54 +01:00
commit af96432c1e
147 changed files with 10579 additions and 1106 deletions

View File

@ -5,7 +5,6 @@ php:
env:
- TESTDB=MYSQL
- TESTDB=PGSQL
- TESTDB=SQLITE
matrix:

View File

@ -13,7 +13,7 @@ class CMSBatchActionHandler extends RequestHandler {
static $url_handlers = array(
'$BatchAction/applicablepages' => 'handleApplicablePages',
'$BatchAction/confirmation' => 'handleConfirmation',
'$BatchAction' => 'handleAction',
'$BatchAction' => 'handleBatchAction',
);
protected $parentController;
@ -66,7 +66,7 @@ class CMSBatchActionHandler extends RequestHandler {
return Controller::join_links($this->parentController->Link(), $this->urlSegment);
}
public function handleAction($request) {
public function handleBatchAction($request) {
// This method can't be called without ajax.
if(!$request->isAjax()) {
$this->parentController->redirectBack();

View File

@ -218,3 +218,9 @@ table.ss-gridfield-table tr.ss-gridfield-item.even { background: #F0F4F7; }
.cms .cms-content-actions .Actions .action-menus.ss-ui-action-tabset ul.ui-tabs-nav .ui-state-active a.ui-tabs-anchor { background: transparent url(../images/sprites-32x32/arrow_up_lighter.png) no-repeat right top; }
.cms .cms-content-actions .Actions .action-menus.ss-ui-action-tabset ul.ui-tabs-nav .ui-state-active a.ui-tabs-anchor:hover { background: transparent url(../images/sprites-32x32/arrow_up_darker.png) no-repeat right top; }
.cms .cms-content-actions .Actions .action-menus.ss-ui-action-tabset .ui-tabs-panel button.ss-ui-button { width: 190px; /* Width 100% not calculating by ie7 */ }
/* Insert Media Area */
.ui-dialog-titlebar { z-index: 100000; }
.ss-uploadfield-item-info .dimensions input { float: left; width: 150px; }
.ss-uploadfield-item-info .dimensions .fieldgroup-field.last { margin-left: 16px; }

View File

@ -192,7 +192,7 @@ form.small .field input.text, form.small .field textarea, form.small .field sele
.field .chzn-container-single .chzn-single div b { background-position: 4px 0px; }
.field .chzn-choices { -webkit-border-radius: 3px; -moz-border-radius: 3px; -ms-border-radius: 3px; -o-border-radius: 3px; border-radius: 3px; }
.field input.month, .field input.day, .field input.year { width: 56px; }
.field input.time { width: 64px; }
.field input.time { width: 88px; }
.field.remove-splitter { border-bottom: none; box-shadow: none; }
/** ---------------------------------------------------- Buttons ---------------------------------------------------- */
@ -212,6 +212,7 @@ form.small .field input.text, form.small .field textarea, form.small .field sele
.cms .ss-ui-button { margin-top: 0px; font-weight: bold; text-decoration: none; line-height: 16px; color: #393939; border: 1px solid #c0c0c2; border-bottom: 1px solid #a6a6a9; cursor: pointer; background-color: #e6e6e6; white-space: nowrap; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2Q5ZDlkOSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background: -webkit-linear-gradient(#ffffff, #d9d9d9); background: -moz-linear-gradient(#ffffff, #d9d9d9); background: -o-linear-gradient(#ffffff, #d9d9d9); background: linear-gradient(#ffffff, #d9d9d9); text-shadow: white 0 1px 1px; /* constructive */ /* destructive */ }
.cms .ss-ui-button.ui-state-hover, .cms .ss-ui-button:hover { text-decoration: none; background-color: white; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2U2ZTZlNiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: -webkit-linear-gradient(#ffffff, #e6e6e6); background: -moz-linear-gradient(#ffffff, #e6e6e6); background: -o-linear-gradient(#ffffff, #e6e6e6); background: linear-gradient(#ffffff, #e6e6e6); -webkit-box-shadow: 0 0 5px #b3b3b3; -moz-box-shadow: 0 0 5px #b3b3b3; box-shadow: 0 0 5px #b3b3b3; }
.cms .ss-ui-button:active, .cms .ss-ui-button:focus, .cms .ss-ui-button.ui-state-active, .cms .ss-ui-button.ui-state-focus { border: 1px solid #b3b3b3; background-color: white; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2U2ZTZlNiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: -webkit-linear-gradient(#ffffff, #e6e6e6); background: -moz-linear-gradient(#ffffff, #e6e6e6); background: -o-linear-gradient(#ffffff, #e6e6e6); background: linear-gradient(#ffffff, #e6e6e6); -webkit-box-shadow: 0 0 5px #b3b3b3 inset; -moz-box-shadow: 0 0 5px #b3b3b3 inset; box-shadow: 0 0 5px #b3b3b3 inset; }
.cms .ss-ui-button.ss-ui-action-minor span { padding-left: 0; padding-right: 0; }
.cms .ss-ui-button.ss-ui-action-constructive { text-shadow: none; font-weight: bold; color: white; border-color: #1f9433; border-bottom-color: #166a24; background-color: #1f9433; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzkzYmU0MiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzFmOTQzMyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #93be42), color-stop(100%, #1f9433)); background: -webkit-linear-gradient(#93be42, #1f9433); background: -moz-linear-gradient(#93be42, #1f9433); background: -o-linear-gradient(#93be42, #1f9433); background: linear-gradient(#93be42, #1f9433); text-shadow: #1c872f 0 -1px -1px; }
.cms .ss-ui-button.ss-ui-action-constructive.ui-state-hover, .cms .ss-ui-button.ss-ui-action-constructive:hover { border-color: #166a24; background-color: #1f9433; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2E0Y2EzYSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzIzYTkzYSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #a4ca3a), color-stop(100%, #23a93a)); background: -webkit-linear-gradient(#a4ca3a, #23a93a); background: -moz-linear-gradient(#a4ca3a, #23a93a); background: -o-linear-gradient(#a4ca3a, #23a93a); background: linear-gradient(#a4ca3a, #23a93a); }
.cms .ss-ui-button.ss-ui-action-constructive:active, .cms .ss-ui-button.ss-ui-action-constructive:focus, .cms .ss-ui-button.ss-ui-action-constructive.ui-state-active, .cms .ss-ui-button.ss-ui-action-constructive.ui-state-focus { background-color: #1d8c30; -webkit-box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); -moz-box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); }
@ -230,7 +231,6 @@ form.small .field input.text, form.small .field textarea, form.small .field sele
.fieldgroup .fieldgroup-field { float: left; display: block; padding: 8px 8px 0 0; }
.fieldgroup .fieldgroup-field .field { border: none; padding-bottom: 0; }
.fieldgroup .fieldgroup-field label { margin-left: 0; margin-right: 1em; width: auto; }
.fieldgroup .fieldgroup-field input, .fieldgroup .fieldgroup-field textarea { float: right; }
.fieldgroup.stacked .fieldgroup-field { float: none; }
.ss-toggle { margin: 8px 0; }
@ -468,7 +468,7 @@ body.cms { overflow: hidden; }
#PageType ul li input, #PageType ul li label, #PageType ul li .page-icon, #PageType ul li .title { float: left; line-height: 1.3em; }
#PageType ul li .page-icon { margin: 0 4px; }
#PageType ul li .title { width: 120px; font-weight: bold; padding-right: 10px; }
#PageType ul li .description { font-style: italic; }
#PageType ul li .description { font-style: italic; display: inline; clear: none; margin: 0; }
/** -------------------------------------------- Content toolbar -------------------------------------------- */
.cms-content-toolbar { min-height: 29px; display: block; margin: 0 0 8px 0; border-bottom: 1px solid #d0d3d5; -webkit-box-shadow: 0 1px 0 rgba(248, 248, 248, 0.9); -moz-box-shadow: 0 1px 0 rgba(248, 248, 248, 0.9); -o-box-shadow: 0 1px 0 rgba(248, 248, 248, 0.9); box-shadow: 0 1px 0 rgba(248, 248, 248, 0.9); *zoom: 1; /* smaller treedropdown */ }
@ -514,16 +514,19 @@ body.cms { overflow: hidden; }
/** CMS Batch actions */
.cms-content-batchactions { float: left; position: relative; display: block; }
.cms-content-batchactions .view-mode-batchactions-wrapper { float: left; padding: 4px 6px; border: 1px solid #aaa; margin-bottom: 8px; background-color: #D9D9D9; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2Q5ZDlkOSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background-image: -webkit-linear-gradient(top, #ffffff, #d9d9d9); background-image: -moz-linear-gradient(top, #ffffff, #d9d9d9); background-image: -o-linear-gradient(top, #ffffff, #d9d9d9); background-image: linear-gradient(top, #ffffff, #d9d9d9); border-top-left-radius: 4px; border-bottom-left-radius: 4px; }
.cms-content-batchactions .view-mode-batchactions-wrapper label { display: none; }
.cms-content-batchactions .view-mode-batchactions-wrapper { height: 18px; float: left; padding: 4px 6px; border: 1px solid #aaa; margin-bottom: 8px; background-color: #D9D9D9; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2Q5ZDlkOSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background-image: -webkit-linear-gradient(top, #ffffff, #d9d9d9); background-image: -moz-linear-gradient(top, #ffffff, #d9d9d9); background-image: -o-linear-gradient(top, #ffffff, #d9d9d9); background-image: linear-gradient(top, #ffffff, #d9d9d9); border-top-left-radius: 4px; border-bottom-left-radius: 4px; }
.cms-content-batchactions .view-mode-batchactions-wrapper input { vertical-align: middle; }
.cms-content-batchactions .view-mode-batchactions-wrapper label { vertical-align: middle; display: none; }
.cms-content-batchactions .view-mode-batchactions-wrapper fieldset, .cms-content-batchactions .view-mode-batchactions-wrapper .Actions { display: inline-block; }
.cms-content-batchactions.inactive .view-mode-batchactions-wrapper { border-radius: 4px; }
.cms-content-batchactions.inactive .view-mode-batchactions-wrapper label { display: inline; }
.cms-content-batchactions form > * { display: block; float: left; }
.cms-content-batchactions form.cms-batch-actions { float: left; }
.cms-content-batchactions.inactive form { display: none; }
.cms-content-batchactions .chzn-container-single { display: block; }
.cms-content-batchactions .chzn-container-single .chzn-single { margin-left: -1px; border-radius: 0; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2Q5ZDlkOSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background-image: -webkit-linear-gradient(top, #ffffff, #d9d9d9); background-image: -moz-linear-gradient(top, #ffffff, #d9d9d9); background-image: -o-linear-gradient(top, #ffffff, #d9d9d9); background-image: linear-gradient(top, #ffffff, #d9d9d9); }
.cms-content-batchactions .cms-batch-actions .Actions .ss-ui-button { margin-left: -1px; border-top-left-radius: 0; border-bottom-left-radius: 0; }
.cms-content-batchactions .chzn-container-single .chzn-single span { padding-top: 0; }
.cms-content-batchactions .cms-batch-actions .Actions .ss-ui-button { padding-top: 4px; padding-bottom: 4px; height: 28px; margin-left: -1px; border-top-left-radius: 0; border-bottom-left-radius: 0; }
.cms-content-batchactions .cms-batch-actions .Actions .ss-ui-button.ui-state-disabled { opacity: 1; color: #ccc; }
#Form_BatchActionsForm select { width: 200px; }
@ -925,17 +928,17 @@ li.class-ErrorPage > a a .jstree-pageicon { background-position: 0 -112px; }
.cms-preview.mobile .preview-scroll, .cms-preview.mobileLandscape .preview-scroll, .cms-preview.tablet .preview-scroll, .cms-preview.tabletLandscape .preview-scroll, .cms-preview.desktop .preview-scroll { background-color: #eceff1; /* cover website preview icon */ }
.cms-preview.mobile .preview-scroll .preview-device-outer, .cms-preview.mobileLandscape .preview-scroll .preview-device-outer, .cms-preview.tablet .preview-scroll .preview-device-outer, .cms-preview.tabletLandscape .preview-scroll .preview-device-outer, .cms-preview.desktop .preview-scroll .preview-device-outer { -webkit-border-radius: 7px; -moz-border-radius: 7px; -ms-border-radius: 7px; -o-border-radius: 7px; border-radius: 7px; background: #d5dde2; border: 1px solid transparent; border-left: 1px solid #cfd9de; padding: 0 16px 16px; }
.cms-preview.mobile .preview-scroll .preview-device-outer .preview-device-inner, .cms-preview.mobileLandscape .preview-scroll .preview-device-outer .preview-device-inner, .cms-preview.tablet .preview-scroll .preview-device-outer .preview-device-inner, .cms-preview.tabletLandscape .preview-scroll .preview-device-outer .preview-device-inner, .cms-preview.desktop .preview-scroll .preview-device-outer .preview-device-inner { border-top: 2px solid #e1e7ea; border-right: 1px solid transparent; border-bottom: 1px solid #e1e7ea; border-left: 1px solid #c3cfd6; }
.cms-preview.mobile .preview-scroll .preview-device-outer { -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); -ms-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); -webkit-transition: all 0.3s ease-in; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-in 1s; -o-transition: all 0.3s ease-in 1s; transition: all 0.3s ease-in 1s; height: 568px; margin: 20px auto 20px; overflow: hidden; padding-top: 16px; width: 335px; }
.cms-preview.mobile .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); -ms-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; width: 335px; }
.cms-preview.mobile .preview-scroll .preview-device-outer { -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); -ms-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); -webkit-transition: all 0.3s ease-in; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-in 1s; -o-transition: all 0.3s ease-in 1s; transition: all 0.3s ease-in 1s; margin: 20px auto 20px; overflow: hidden; padding-top: 16px; }
.cms-preview.mobile .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); -ms-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; }
.cms-preview.mobile .preview-scroll .preview-device-outer.rotate { -webkit-transform: rotate(-90deg); -moz-transform: rotate(-90deg); -ms-transform: rotate(-90deg); -o-transform: rotate(-90deg); transform: rotate(-90deg); -webkit-transition: all 0.3s ease-in; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-in 1s; -o-transition: all 0.3s ease-in 1s; transition: all 0.3s ease-in 1s; height: 583px; margin: 0px auto 0px; width: 320px; }
.cms-preview.mobile .preview-scroll .preview-device-outer.rotate .preview-device-inner { -webkit-transform-origin: 160px 160px; -moz-transform-origin: 160px 160px; -ms-transform-origin: 160px 160px; -o-transform-origin: 160px 160px; transform-origin: 160px 160px; -webkit-transform: rotate(90deg); -moz-transform: rotate(90deg); -ms-transform: rotate(90deg); -o-transform: rotate(90deg); transform: rotate(90deg); -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; height: 320px; width: 583px; }
.cms-preview.mobileLandscape .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; height: 320px; margin: 12% auto; padding-top: 16px; width: 583px; }
.cms-preview.mobileLandscape .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; width: 583px; }
.cms-preview.tablet .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; height: 1024px; margin: 0 auto; width: 783px; }
.cms-preview.tablet .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; width: 783px; }
.cms-preview.tabletLandscape .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; height: 768px; margin: 0 auto; width: 1039px; }
.cms-preview.tabletLandscape .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; width: 1039px; }
.cms-preview.desktop .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; height: 800px; margin: 0 auto; width: 1024px; }
.cms-preview.mobileLandscape .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; margin: 12% auto; padding-top: 16px; }
.cms-preview.mobileLandscape .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; }
.cms-preview.tablet .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; margin: 0 auto; }
.cms-preview.tablet .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; }
.cms-preview.tabletLandscape .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; margin: 0 auto; }
.cms-preview.tabletLandscape .preview-scroll .preview-device-outer .preview-device-inner { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; }
.cms-preview.desktop .preview-scroll .preview-device-outer { -webkit-transition: all 0.3s ease-out; -webkit-transition-delay: 1s; -moz-transition: all 0.3s ease-out 1s; -o-transition: all 0.3s ease-out 1s; transition: all 0.3s ease-out 1s; margin: 0 auto; }
/********************************************
* Defines the styles for .ss-ui-action-tabset:

View File

@ -108,7 +108,10 @@
// specifically opt-out of this behaviour via "data-skip-autofocus".
// This opt-out is useful if the first visible field is shown far down a scrollable area,
// for example for the pagination input field after a long GridField listing.
this.find(':input:not(:submit)[data-skip-autofocus!="true"]').filter(':visible:first').focus();
// Skip if an element in the form is already focused.
if(!this.find(document.activeElement).length) {
this.find(':input:not(:submit)[data-skip-autofocus!="true"]').filter(':visible:first').focus();
}
},
onunmatch: function() {
this._super();

View File

@ -17,7 +17,7 @@
* The order is significant - if the state is not available, preview will start searching the list
* from the beginning.
*/
AllowedStates: ['StageLink', 'LiveLink'],
AllowedStates: ['StageLink', 'LiveLink','ArchiveLink'],
/**
* API
@ -36,12 +36,54 @@
*/
IsPreviewEnabled: false,
/**
* Mode in which the preview will be enabled.
*/
DefaultMode: 'split',
Sizes: {
auto: {
width: '100%',
height: '100%'
},
mobile: {
width: '335px', // add 15px for approx desktop scrollbar
height: '568px'
},
mobileLandscape: {
width: '583px', // add 15px for approx desktop scrollbar
height: '320px'
},
tablet: {
width: '783px', // add 15px for approx desktop scrollbar
height: '1024px'
},
tabletLandscape: {
width: '1039px', // add 15px for approx desktop scrollbar
height: '768px'
},
desktop: {
width: '1024px',
height: '800px'
}
},
/**
* API
* Switch the preview to different state.
* stateName can be one of the "AllowedStates".
*
* @param {String}
* @param {Boolean} Set to FALSE to avoid persisting the state
*/
changeState: function(stateName) {
changeState: function(stateName, save) {
var self = this, states = this._getNavigatorStates();
if(save !== false) {
$.each(states, function(index, state) {
self.saveState('state', stateName);
});
}
this.setCurrentStateName(stateName);
this._loadCurrentState();
this.redraw();
@ -54,17 +96,26 @@
* Change the preview mode.
* modeName can be: split, content, preview.
*/
changeMode: function(modeName) {
changeMode: function(modeName, save) {
var container = $('.cms-container');
if (modeName === 'split') {
if (modeName == 'split') {
container.entwine('.ss').splitViewMode();
} else if (modeName === 'content') {
this.setIsPreviewEnabled(true);
this._loadCurrentState();
} else if (modeName == 'content') {
container.entwine('.ss').contentViewMode();
} else {
this.setIsPreviewEnabled(false);
this._loadCurrentState();
} else if (modeName == 'preview') {
container.entwine('.ss').previewMode();
this.setIsPreviewEnabled(true);
} else {
throw 'Invalid mode: ' + modeName;
}
if(save !== false) this.saveState('mode', modeName);
this.redraw();
return this;
@ -76,10 +127,17 @@
* sizeName can be: auto, desktop, tablet, mobile.
*/
changeSize: function(sizeName) {
this.setCurrentSizeName(sizeName);
var sizes = this.getSizes();
this.removeClass('auto desktop tablet mobile')
.addClass(sizeName);
this.setCurrentSizeName(sizeName);
this.removeClass('auto desktop tablet mobile').addClass(sizeName);
this.find('.preview-device-outer')
.width(sizes[sizeName].width)
.height(sizes[sizeName].height);
this.find('.preview-device-inner')
.width(sizes[sizeName].width);
this.saveState('size', sizeName);
this.redraw();
@ -116,6 +174,24 @@
return this;
},
/**
* Store the preview options for this page.
*/
saveState : function(name, value) {
if(!window.localStorage) return;
window.localStorage.setItem('cms-preview-state-' + name, value);
},
/**
* Load previously stored preferences
*/
loadState : function(name) {
if(!window.localStorage) return;
return window.localStorage.getItem('cms-preview-state-' + name);
},
/**
* Disable the area - it will not appear in the GUI.
* Caveat: the preview will be automatically enabled when ".cms-previewable" class is detected.
@ -123,7 +199,7 @@
disablePreview: function() {
this._loadUrl('about:blank');
this._block();
this.changeMode('content');
this.changeMode('content', false);
this.setIsPreviewEnabled(false);
return this;
},
@ -140,7 +216,7 @@
// We do not support the split mode in IE < 8.
this.changeMode('content');
} else {
this.changeMode('split');
this.changeMode(this.getDefaultMode(), false);
}
}
return this;
@ -178,6 +254,7 @@
*/
_block: function() {
this.addClass('blocked');
this.find('.cms-preview-overlay').show();
return this;
},
@ -186,6 +263,7 @@
*/
_unblock: function() {
this.removeClass('blocked');
this.find('.cms-preview-overlay').hide();
return this;
},
@ -193,13 +271,25 @@
* Update the preview according to browser and CMS section capabilities.
*/
_initialiseFromContent: function() {
var mode, size;
if (!$('.cms-previewable').length) {
this.disablePreview();
} else {
this.enablePreview();
mode = this.loadState('mode');
size = this.loadState('size');
this._moveNavigator();
this._loadCurrentState();
if(!mode || mode != 'content') {
this.enablePreview();
this._loadCurrentState();
}
this.redraw();
// now check the cookie to see if we have any preview settings that have been
// retained for this page from the last visit
if(mode) this.changeMode(mode);
if(size) this.changeSize(size);
}
return this;
},
@ -225,7 +315,7 @@
},
/**
* Change the URL of the preview iframe.
* Change the URL of the preview iframe (if its not already displayed).
*/
_loadUrl: function(url) {
this.find('iframe').addClass('loading').attr('src', url);
@ -240,7 +330,15 @@
// Walk through available states and get the URLs.
var urlMap = $.map(this.getAllowedStates(), function(name) {
var stateLink = $('.cms-preview-states .state-name[data-name=' + name + ']');
return stateLink.length ? {name: name, url: stateLink.attr('data-link')} : null;
if(stateLink.length) {
return {
name: name,
url: stateLink.attr('data-link'),
active: stateLink.is(':radio') ? stateLink.is(':checked') : stateLink.is(':selected')
};
} else {
return null;
}
});
return urlMap;
@ -263,7 +361,10 @@
// Find current state within currently available states.
if (states) {
currentState = $.grep(states, function(state, index) {
return currentStateName===state.name;
return (
currentStateName === state.name ||
(!currentStateName && state.active)
);
});
}
@ -361,20 +462,6 @@
}
});
/**
* Update the "preview unavailable" overlay according to the class.
*/
$('.cms-preview.blocked').entwine({
onmatch: function() {
this.find('.cms-preview-overlay').show();
this._super();
},
onunmatch: function() {
this.find('.cms-preview-overlay').hide();
this._super();
}
});
/**
* "Preview state" functions.
* -------------------------------------------------------------------

View File

@ -286,10 +286,9 @@
// Copy attributes. We can't replace the node completely
// without removing or detaching its children nodes.
for(var i=0; i<newNode[0].attributes.length; i++){
var attr = newNode[0].attributes[i];
node.attr(attr.name, attr.value);
}
$.each(['id', 'style', 'class', 'data-pagetype'], function(i, attrName) {
node.attr(attrName, newNode.attr(attrName));
});
// Replace inner content
var origChildren = node.children('ul').detach();

View File

@ -168,7 +168,8 @@ jQuery.noConflict();
* Ensure the user can see the requested section - restore the default view.
*/
'from .cms-menu-list li a': {
onclick: function() {
onclick: function(e) {
if(e.which > 1) return;
this.splitViewMode();
}
},
@ -1050,6 +1051,7 @@ jQuery.noConflict();
}(jQuery));
var statusMessage = function(text, type) {
text = $('<div/>').text(text).html(); // Escape HTML entities in text
jQuery.noticeAdd({text: text, type: type});
};

View File

@ -7,6 +7,6 @@ if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
'ModelAdmin.DELETED': "Gelöscht",
'ModelAdmin.VALIDATIONERROR': "Validationsfehler",
'LeftAndMain.PAGEWASDELETED': "Diese Seite wurde gelöscht.",
'LeftAndMain.CONFIRMUNSAVED': "Sind Sie sicher, dasß Sie die Seite verlassen möchten?\n\nWARNUNG: Ihre Änderungen werden nicht gespeichert.\n\nDrücken Sie \"OK\" um fortzufahren, oder \"Abbrechen\" um auf dieser Seite zu bleiben."
'LeftAndMain.CONFIRMUNSAVED': "Sind Sie sicher, dass Sie die Seite verlassen möchten?\n\nWARNUNG: Ihre Änderungen werden nicht gespeichert.\n\nDrücken Sie \"OK\" um fortzufahren, oder \"Abbrechen\" um auf dieser Seite zu bleiben."
});
}
}

View File

@ -0,0 +1,14 @@
if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
if(typeof(console) != 'undefined') console.error('Class ss.i18n not defined');
} else {
ss.i18n.addDictionary('nl_NL', {
'LeftAndMain.CONFIRMUNSAVED': "Weet u zeker dat u deze pagina wilt verlaten?\n\WAARSCHUWING: Uw veranderingen zijn niet opgeslagen.\n\nKies OK om te verlaten, of Cancel om op de huidige pagina te blijven.",
'LeftAndMain.CONFIRMUNSAVEDSHORT': "WAARSCHUWING: Uw veranderingen zijn niet opgeslagen",
'SecurityAdmin.BATCHACTIONSDELETECONFIRM': "Weet u zeker dat u deze groep %s wilt verwijderen?",
'ModelAdmin.SAVED': "Opgeslagen",
'ModelAdmin.REALLYDELETE': "Weet u zeker dat u wilt verwijderen?",
'ModelAdmin.DELETED': "Verwijderd",
'ModelAdmin.VALIDATIONERROR': "Validatie fout",
'LeftAndMain.PAGEWASDELETED': "Deze pagina is verwijderd. Om een pagina aan te passen, selecteer pagina aan de linkerkant."
});
}

View File

@ -265,7 +265,7 @@ form.small .field, .field.small {
}
input.time {
width: ($grid-x * 8); // smaller time field, since input is restricted
width: ($grid-x * 11); // smaller time field, since input is restricted
}
/* Hides borders in settings/access. Activated from JS */
@ -404,6 +404,13 @@ form.small .field, .field.small {
);
@include box-shadow(0 0 5px darken($color-button-generic, 20%) inset);
}
&.ss-ui-action-minor {
span {
padding-left: 0;
padding-right: 0;
}
}
/* constructive */
&.ss-ui-action-constructive {
@ -515,10 +522,6 @@ form.small .field, .field.small {
margin-right: 1em;
width: auto;
}
input, textarea {
float: right;
}
}
&.stacked {

View File

@ -284,15 +284,12 @@
.preview-scroll .preview-device-outer {
@include rotate(0deg);
@include transition(all 0.3s ease-in 1s);
height: 568px;
margin: 20px auto 20px;
overflow:hidden;
padding-top: 16px;
width: 335px; // add 15px for approx desktop scrollbar
.preview-device-inner {
@include rotate(0deg);
@include transition(all 0.3s ease-out 1s);
width: 335px;
}
&.rotate {
@include rotate(-90deg);
@ -312,39 +309,28 @@
}
&.mobileLandscape .preview-scroll .preview-device-outer {
@include transition(all 0.3s ease-out 1s);
height: 320px;
margin: 12% auto;
padding-top: 16px;
width: 583px; // add 15px for approx desktop scrollbar
.preview-device-inner {
@include transition(all 0.3s ease-out 1s);
width: 583px;
}
}
&.tablet .preview-scroll .preview-device-outer {
@include transition(all 0.3s ease-out 1s);
height: 1024px;
margin: 0 auto;
width: 783px; // add 15px for approx desktop scrollbar
.preview-device-inner {
@include transition(all 0.3s ease-out 1s);
width: 783px;
}
}
&.tabletLandscape .preview-scroll .preview-device-outer {
@include transition(all 0.3s ease-out 1s);
height: 768px;
margin: 0 auto;
width: 1039px;// add 15px for approx desktop scrollbar
.preview-device-inner {
@include transition(all 0.3s ease-out 1s);
width: 1039px;
}
}
&.desktop .preview-scroll .preview-device-outer {
@include transition(all 0.3s ease-out 1s);
height: 800px;
margin: 0 auto;
width: 1024px;
}
}

View File

@ -582,6 +582,10 @@ body.cms {
.description {
font-style: italic;
// Undo some generic styles from tooltips
display: inline;
clear: none;
margin: 0;
}
}
}
@ -823,6 +827,7 @@ body.cms {
display: block;
.view-mode-batchactions-wrapper {
height: 18px;
float: left;
padding: 4px 6px;
border: 1px solid #aaa;
@ -830,9 +835,14 @@ body.cms {
background-color: #D9D9D9;
@include background-image(linear-gradient(top, #fff, #D9D9D9));
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-left-radius: 4px;
input {
vertical-align: middle;
}
label {
vertical-align: middle;
display: none;
}
@ -842,18 +852,18 @@ body.cms {
}
&.inactive .view-mode-batchactions-wrapper {
border-radius: 4px;
border-radius: 4px;
label {
display: inline;
}
}
form > * {
display: block;
float: left;
}
form.cms-batch-actions {
float: left;
}
@ -862,13 +872,24 @@ body.cms {
display: none;
}
.chzn-container-single .chzn-single {
margin-left: -1px;
border-radius: 0;
@include background-image(linear-gradient(top, #fff, #D9D9D9));
.chzn-container-single {
display: block;
.chzn-single {
margin-left: -1px;
border-radius: 0;
@include background-image(linear-gradient(top, #fff, #D9D9D9));
span {
padding-top: 0;
}
}
}
.cms-batch-actions .Actions .ss-ui-button {
padding-top: 4px;
padding-bottom: 4px;
height: 28px;
margin-left: -1px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;

View File

@ -303,3 +303,21 @@ table.ss-gridfield-table {
width: 190px; /* Width 100% not calculating by ie7 */
}
}
/* Insert Media Area */
.ui-dialog-titlebar {
z-index: 100000;
}
.ss-uploadfield-item-info {
.dimensions {
input {
float:left;
width:150px;
}
.fieldgroup-field.last {
margin-left:16px;
}
}
}

View File

@ -27,7 +27,7 @@
<a href="$Link" <% if Code == 'Help' %>target="_blank"<% end_if%>>
<span class="icon icon-16 icon-{$Code.LowerCase}">&nbsp;</span>
<span class="text">$Title</span>
</a>
</a>
</li>
<% end_loop %>
</ul>

View File

@ -24,8 +24,8 @@
<fieldset id="preview-states" class="cms-preview-states switch-states size_{$Items.Count}">
<div class="switch">
<% loop Items %>
<input id="$Title" data-name="$Name" class="state-name $FirstLast" data-link="$Link" name="view" type="radio" <% if First %>checked<% end_if %>>
<label for="$Title"<% if First %> class="active"<% end_if %>><span>$Title</span></label>
<input id="$Title" data-name="$Name" class="state-name $FirstLast" data-link="$Link" name="view" type="radio" <% if isActive %>checked<% end_if %>>
<label for="$Title"<% if isActive %> class="active"<% end_if %>><span>$Title</span></label>
<% end_loop %>
<span class="slide-button"></span>
</div>
@ -34,7 +34,7 @@
<span id="preview-state-dropdown" class="cms-preview-states field dropdown">
<select title="<% _t('SilverStripeNavigator.PreviewState', 'Preview State') %>" id="preview-states" class="preview-state dropdown nolabel" autocomplete="off" name="preview-state">
<% loop Items %>
<option name="$Name" data-name="$Name" data-link="$Link" class="state-name $FirstLast" value="$Link" >
<option name="$Name" data-name="$Name" data-link="$Link" class="state-name $FirstLast" value="$Link" <% if isActive %>selected<% end_if %>>
$Title
</option>
<% end_loop %>

View File

@ -190,17 +190,19 @@ class RSSFeed extends ViewableData {
SSViewer::set_source_file_comments(false);
$response = Controller::curr()->getResponse();
$response = Controller::curr()->getResponse();
if(is_int($this->lastModified)) {
HTTP::register_modification_timestamp($this->lastModified);
$response->addHeader('Last-Modified', gmdate("D, d M Y H:i:s", $this->lastModified) . ' GMT');
$response->addHeader("Last-Modified", gmdate("D, d M Y H:i:s", $this->lastModified) . ' GMT');
}
if(!empty($this->etag)) {
HTTP::register_etag($this->etag);
}
if(!headers_sent()) {
HTTP::add_cache_headers($response);
$response->addHeader('Content-Type', 'text/xml');
HTTP::add_cache_headers();
$response->addHeader("Content-Type", "application/rss+xml");
}
SSViewer::set_source_file_comments($prevState);
@ -294,7 +296,7 @@ class RSSFeed_Entry extends ViewableData {
* @return string Returns the description of the entry.
*/
public function Description() {
return $this->rssField($this->descriptionField, 'Text');
return $this->rssField($this->descriptionField, 'HTMLText');
}
/**

View File

@ -17,6 +17,27 @@ class RestfulService extends ViewableData {
protected $customHeaders = array();
protected $proxy;
protected static $default_proxy;
protected static $default_curl_options = array();
/**
* set a curl option that will be applied to all requests as default
* {@see http://php.net/manual/en/function.curl-setopt.php#refsect1-function.curl-setopt-parameters}
*
* @param int $option The cURL opt Constant
* @param mixed $value The cURL opt value
*/
public static function set_default_curl_option($option, $value) {
self::$default_curl_options[$option] = $value;
}
/**
* set many defauly curl options at once
*/
public static function set_default_curl_options($optionArray) {
foreach ($optionArray as $option => $value) {
self::set_default_curl_option($option, $value);
}
}
/**
* Sets default proxy settings for outbound RestfulService connections
@ -114,12 +135,20 @@ class RestfulService extends ViewableData {
assert(in_array($method, array('GET','POST','PUT','DELETE','HEAD','OPTIONS')));
$cachedir = TEMP_FOLDER; // Default silverstripe cache
$cache_file = md5($url); // Encoded name of cache file
$cache_path = $cachedir."/xmlresponse_$cache_file";
$cache_path = $this->getCachePath(array(
$url,
$method,
$data,
array_merge((array)$this->customHeaders, (array)$headers),
array_merge(self::$default_curl_options,$curlOptions),
$this->getBasicAuthString()
));
// Check for unexpired cached feed (unless flush is set)
if(!isset($_GET['flush']) && @file_exists($cache_path)
//assume any cache_expire that is 0 or less means that we dont want to
// cache
if($this->cache_expire > 0 && !isset($_GET['flush'])
&& @file_exists($cache_path)
&& @filemtime($cache_path) + $this->cache_expire > time()) {
$store = file_get_contents($cache_path);
@ -140,10 +169,10 @@ class RestfulService extends ViewableData {
$store = file_get_contents($cache_path);
$cachedResponse = unserialize($store);
$response->setCachedBody($cachedResponse->getBody());
$response->setCachedResponse($cachedResponse);
}
else {
$response->setCachedBody(false);
$response->setCachedResponse(false);
}
}
}
@ -167,6 +196,7 @@ class RestfulService extends ViewableData {
$timeout = 5;
$sapphireInfo = new SapphireInfo();
$useragent = 'SilverStripe/' . $sapphireInfo->Version();
$curlOptions = array_merge(self::$default_curl_options, $curlOptions);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
@ -174,6 +204,8 @@ class RestfulService extends ViewableData {
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
if(!ini_get('open_basedir')) curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
//include headers in the response
curl_setopt($ch, CURLOPT_HEADER, true);
// Add headers
if($this->customHeaders) {
@ -183,7 +215,7 @@ class RestfulService extends ViewableData {
if($headers) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
// Add authentication
if($this->authUsername) curl_setopt($ch, CURLOPT_USERPWD, "$this->authUsername:$this->authPassword");
if($this->authUsername) curl_setopt($ch, CURLOPT_USERPWD, $this->getBasicAuthString());
// Add fields to POST and PUT requests
if($method == 'POST') {
@ -208,26 +240,112 @@ class RestfulService extends ViewableData {
curl_setopt_array($ch, $curlOptions);
// Run request
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$responseBody = curl_exec($ch);
$curlError = curl_error($ch);
// Problem verifying the server SSL certificate; just ignore it as it's not mandatory
if(strpos($curlError,'14090086') !== false) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$responseBody = curl_exec($ch);
$curlError = curl_error($ch);
}
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($curlError !== '' || $statusCode == 0) $statusCode = 500;
$response = new RestfulService_Response($responseBody, $statusCode);
$rawResponse = curl_exec($ch);
$response = $this->extractResponse($ch, $rawResponse);
curl_close($ch);
return $response;
}
/**
* A function to return the auth string. This helps consistency through the
* class but also allows tests to pull it out when generating the expected
* cache keys
*
* @see {self::getCachePath()}
* @see {RestfulServiceTest::createFakeCachedResponse()}
*
* @return string The auth string to be base64 encoded
*/
protected function getBasicAuthString() {
return $this->authUsername . ':' . $this->authPassword;
}
/**
* Generate a cache key based on any cache data sent. The cache data can be
* any type
*
* @param mixed $cacheData The cache seed for generating the key
* @param string the md5 encoded cache seed.
*/
protected function generateCacheKey($cacheData) {
return md5(var_export($cacheData, true));
}
/**
* Generate the cache path
*
* This is mainly so that the cache path can be generated in a consistent
* way in tests without having to hard code the cachekey generate function
* in tests
*
* @param mixed $cacheData The cache seed {@see self::generateCacheKey}
*
* @return string The path to the cache file
*/
protected function getCachePath($cacheData) {
return TEMP_FOLDER . "/xmlresponse_" . $this->generateCacheKey($cacheData);
}
/**
* Extracts the response body and headers from a full curl response
*
* @param curl_handle $ch The curl handle for the request
* @param string $rawResponse The raw response text
*
* @return RestfulService_Response The response object
*/
protected function extractResponse($ch, $rawResponse) {
//get the status code
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
//normalise the status code
if($curlError !== '' || $statusCode == 0) $statusCode = 500;
//calculate the length of the header and extract it
$headerLength = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$rawHeaders = substr($rawResponse, 0, $headerLength);
//extract the body
$body = substr($rawResponse, $headerLength);
//parse the headers
$headers = $this->parseRawHeaders($rawHeaders);
//return the response object
return new RestfulService_Response($body, $statusCode, $headers);
}
/**
* Takes raw headers and parses them to turn them to an associative array
*
* Any header that we see more than once is turned into an array.
*
* This is meant to mimic http_parse_headers {@link http://php.net/manual/en/function.http-parse-headers.php}
* thanks to comment #77241 on that page for foundation of this
*
* @param string $rawHeaders The raw header string
* @return array The assosiative array of headers
*/
protected function parseRawHeaders($rawHeaders) {
$headers = array();
$fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $rawHeaders));
foreach( $fields as $field ) {
if( preg_match('/([^:]+): (.+)/m', $field, $match) ) {
$match[1] = preg_replace_callback(
'/(?<=^|[\x09\x20\x2D])./',
create_function('$matches', 'return strtoupper($matches[0]);'),
strtolower(trim($match[1]))
);
if( isset($headers[$match[1]]) ) {
if (!is_array($headers[$match[1]])) {
$headers[$match[1]] = array($headers[$match[1]]);
}
$headers[$match[1]][] = $match[2];
} else {
$headers[$match[1]] = trim($match[2]);
}
}
}
return $headers;
}
/**
* Returns a full request url
* @param string
@ -418,10 +536,10 @@ class RestfulService_Response extends SS_HTTPResponse {
protected $simpleXML;
/**
* @var boolean It should be populated with cached content
* @var boolean It should be populated with cached request
* when a request referring to this response was unsuccessful
*/
protected $cachedBody = false;
protected $cachedResponse = false;
public function __construct($body, $statusCode = 200, $headers = null) {
$this->setbody($body);
@ -441,18 +559,44 @@ class RestfulService_Response extends SS_HTTPResponse {
return $this->simpleXML;
}
/**
* get the cached response object. This allows you to access the cached
* eaders, not just the cached body.
*
* @return RestfulSerivice_Response The cached response object
*/
public function getCachedResponse() {
return $this->cachedResponse;
}
/**
* @return string
*/
public function getCachedBody() {
return $this->cachedBody;
if ($this->cachedResponse) {
return $this->cachedResponse->getBody();
}
return false;
}
/**
* @param string
*/
public function setCachedBody($content) {
Deprecation::notice('3.1', 'Setting the response body is now deprecated, set the cached request instead');
if (!$this->cachedResponse) {
$this->cachedResponse = new RestfulService_Response($content);
}
else {
$this->cachedResponse->setBody = $content;
}
}
/**
* @param string
*/
public function setCachedBody($content) {
$this->cachedBody = $content;
public function setCachedResponse($response) {
$this->cachedResponse = $response;
}
/**
@ -467,8 +611,8 @@ class RestfulService_Response extends SS_HTTPResponse {
*/
public function xpath_one($xpath) {
$items = $this->xpath($xpath);
return $items[0];
if (isset($items[0])) {
return $items[0];
}
}
}

View File

@ -41,6 +41,7 @@
*
* Email:
* - SS_SEND_ALL_EMAILS_TO: If you set this define, all emails will be redirected to this address.
* - SS_SEND_ALL_EMAILS_FROM: If you set this define, all emails will be send from this address.
*
* @package framework
* @subpackage core
@ -105,7 +106,10 @@ if(defined('SS_DATABASE_USERNAME') && defined('SS_DATABASE_PASSWORD')) {
}
if(defined('SS_SEND_ALL_EMAILS_TO')) {
Email::send_all_emails_to(SS_SEND_ALL_EMAILS_TO);
Config::inst()->update("Email","send_all_emails_to", SS_SEND_ALL_EMAILS_TO);
}
if(defined('SS_SEND_ALL_EMAILS_FROM')) {
Config::inst()->update("Email","send_all_emails_from", SS_SEND_ALL_EMAILS_FROM);
}
if(defined('SS_DEFAULT_ADMIN_USERNAME')) {

View File

@ -182,36 +182,25 @@ class Controller extends RequestHandler implements TemplateGlobalProvider {
* Controller's default action handler. It will call the method named in $Action, if that method exists.
* If $Action isn't given, it will use "index" as a default.
*/
public function handleAction($request) {
// urlParams, requestParams, and action are set for backward compatability
public function handleAction($request, $action) {
foreach($request->latestParams() as $k => $v) {
if($v || !isset($this->urlParams[$k])) $this->urlParams[$k] = $v;
}
$this->action = str_replace("-","_",$request->param('Action'));
$this->action = $action;
$this->requestParams = $request->requestVars();
if(!$this->action) $this->action = 'index';
if(!$this->hasAction($this->action)) {
$this->httpError(404, "The action '$this->action' does not exist in class $this->class");
}
// run & init are manually disabled, because they create infinite loops and other dodgy situations
if(!$this->checkAccessAction($this->action) || in_array(strtolower($this->action), array('run', 'init'))) {
return $this->httpError(403, "Action '$this->action' isn't allowed on class $this->class");
}
if($this->hasMethod($this->action)) {
$result = $this->{$this->action}($request);
if($this->hasMethod($action)) {
$result = parent::handleAction($request, $action);
// If the action returns an array, customise with it before rendering the template.
if(is_array($result)) {
return $this->getViewer($this->action)->process($this->customise($result));
return $this->getViewer($action)->process($this->customise($result));
} else {
return $result;
}
} else {
return $this->getViewer($this->action)->process($this);
return $this->getViewer($action)->process($this);
}
}
@ -454,7 +443,7 @@ class Controller extends RequestHandler implements TemplateGlobalProvider {
public function redirect($url, $code=302) {
if(!$this->response) $this->response = new SS_HTTPResponse();
if($this->response->getHeader('Location')) {
if($this->response->getHeader('Location') && $this->response->getHeader('Location') != $url) {
user_error("Already directed to " . $this->response->getHeader('Location')
. "; now trying to direct to $url", E_USER_WARNING);
return;

View File

@ -141,18 +141,7 @@ class Director implements TemplateGlobalProvider {
$res = Injector::inst()->get('RequestProcessor')->postRequest($req, $response, $model);
if ($res !== false) {
// Set content length (according to RFC2616)
if(
!headers_sent()
&& $response->getBody()
&& $req->httpMethod() != 'HEAD'
&& $response->getStatusCode() >= 200
&& !in_array($response->getStatusCode(), array(204, 304))
) {
$response->fixContentLength();
}
$response->output();
$response->output();
} else {
// @TODO Proper response here.
throw new SS_HTTPResponse_Exception("Invalid response");
@ -674,6 +663,9 @@ class Director implements TemplateGlobalProvider {
$matched = false;
if($patterns) {
// Calling from the command-line?
if(!isset($_SERVER['REQUEST_URI'])) return;
// protect portions of the site based on the pattern
$relativeURL = self::makeRelative(Director::absoluteURL($_SERVER['REQUEST_URI']));
foreach($patterns as $pattern) {

View File

@ -41,20 +41,43 @@ class HTTP {
*/
public static function absoluteURLs($html) {
$html = str_replace('$CurrentPageURL', $_SERVER['REQUEST_URI'], $html);
return HTTP::urlRewriter($html, '(substr($URL,0,1) == "/") ? ( Director::protocolAndHost() . $URL ) :'
. ' ( (preg_match("/^[A-Za-z]+:/", $URL)) ? $URL : Director::absoluteBaseURL() . $URL )' );
return HTTP::urlRewriter($html, function($url) {
return Director::absoluteURL($url, true);
});
}
/*
* Rewrite all the URLs in the given content, evaluating the given string as PHP code
/**
* Rewrite all the URLs in the given content, evaluating the given string as PHP code.
*
* Put $URL where you want the URL to appear, however, you can't embed $URL in strings
* Some example code:
* '"../../" . $URL'
* 'myRewriter($URL)'
* '(substr($URL,0,1)=="/") ? "../" . substr($URL,1) : $URL'
* <ul>
* <li><code>'"../../" . $URL'</code></li>
* <li><code>'myRewriter($URL)'</code></li>
* <li><code>'(substr($URL,0,1)=="/") ? "../" . substr($URL,1) : $URL'</code></li>
* </ul>
*
* As of 3.2 $code should be a callable which takes a single parameter and returns
* the rewritten URL. e.g.
*
* <code>
* function($url) {
* return Director::absoluteURL($url, true);
* }
* </code>
*
* @param string $content The HTML to search for links to rewrite
* @param string|callable $code Either a string that can evaluate to an expression
* to rewrite links (depreciated), or a callable that takes a single
* parameter and returns the rewritten URL
* @return The content with all links rewritten as per the logic specified in $code
*/
public static function urlRewriter($content, $code) {
if(!is_callable($code)) {
Deprecation::notice(3.1, 'HTTP::urlRewriter expects a callable as the second parameter');
}
// Replace attributes
$attribs = array("src","background","a" => "href","link" => "href", "base" => "href");
foreach($attribs as $tag => $attrib) {
if(!is_numeric($tag)) $tagPrefix = "$tag ";
@ -64,19 +87,28 @@ class HTTP {
$regExps[] = "/(<{$tagPrefix}[^>]*$attrib *= *')([^']*)(')/i";
$regExps[] = "/(<{$tagPrefix}[^>]*$attrib *= *)([^\"' ]*)( )/i";
}
$regExps[] = '/(background-image:[^;]*url *\()([^)]+)(\))/i';
$regExps[] = '/(background:[^;]*url *\()([^)]+)(\))/i';
$regExps[] = '/(list-style-image:[^;]*url *\()([^)]+)(\))/i';
$regExps[] = '/(list-style:[^;]*url *\()([^)]+)(\))/i';
// Replace css styles
// @todo - http://www.css3.info/preview/multiple-backgrounds/
$styles = array('background-image', 'background', 'list-style-image', 'list-style', 'content');
foreach($styles as $style) {
$regExps[] = "/($style:[^;]*url *\(\")([^\"]+)(\"\))/i";
$regExps[] = "/($style:[^;]*url *\(')([^']+)('\))/i";
$regExps[] = "/($style:[^;]*url *\()([^\"\)')]+)(\))/i";
}
// Make
// Callback for regexp replacement
$callback = function($matches) use($code) {
return
stripslashes($matches[1]) .
str_replace('$URL', stripslashes($matches[2]), $code) .
stripslashes($matches[3]);
if(is_callable($code)) {
$rewritten = $code($matches[2]);
} else {
// Expose the $URL variable to be used by the $code expression
$URL = $matches[2];
$rewritten = eval("return ($code);");
}
return $matches[1] . $rewritten . $matches[3];
};
// Execute each expression
foreach($regExps as $regExp) {
$content = preg_replace_callback($regExp, $callback, $content);
}
@ -282,7 +314,7 @@ class HTTP {
$responseHeaders["Cache-Control"] = "max-age=" . self::$cache_age . ", must-revalidate, no-transform";
$responseHeaders["Pragma"] = "";
// To do: User-Agent should only be added in situations where you *are* actually varying according to user-agent.
// To do: User-Agent should only be added in situations where you *are* actually varying according to it.
$responseHeaders['Vary'] = 'Cookie, X-Forwarded-Protocol, User-Agent, Accept';
} else {
@ -293,11 +325,12 @@ class HTTP {
$responseHeaders["Last-Modified"] = self::gmt_date(self::$modification_date);
/* Chrome ignores Varies when redirecting back (http://code.google.com/p/chromium/issues/detail?id=79758)
which means that if you log out, you get redirected back to a page which Chrome then checks against last-modified (which passes, getting a 304)
when it shouldn't be trying to use that page at all because it's the "logged in" version.
which means that if you log out, you get redirected back to a page which Chrome then checks against
last-modified (which passes, getting a 304) when it shouldn't be trying to use that page at all because
it's the "logged in" version.
By also using and etag that includes both the modification date and all the varies values which we also check against we can catch
this and not return a 304
By also using and etag that includes both the modification date and all the varies values which we also
check against we can catch this and not return a 304
*/
$etagParts = array(self::$modification_date, serialize($_COOKIE));
if (isset($_SERVER['HTTP_X_FORWARDED_PROTOCOL'])) $etagParts[] = $_SERVER['HTTP_X_FORWARDED_PROTOCOL'];

View File

@ -610,7 +610,7 @@ class SS_HTTPRequest implements ArrayAccess {
for($i=0;$i<$count;$i++) {
$value = array_shift($this->dirParts);
if(!$value) break;
if($value === null) break;
$return[] = $value;
}

View File

@ -160,6 +160,9 @@ class SS_HTTPResponse {
*/
public function setBody($body) {
$this->body = $body;
// Set content-length in bytes. Use mbstring to avoid problems with mb_internal_encoding() and mbstring.func_overload
$this->headers['Content-Length'] = mb_strlen($this->body,'8bit');
}
/**
@ -241,11 +244,17 @@ class SS_HTTPResponse {
<meta http-equiv=\"refresh\" content=\"1; url=$url\" />
<script type=\"text/javascript\">setTimeout('window.location.href = \"$url\"', 50);</script>";
} else {
if(!headers_sent()) {
$line = $file = null;
if(!headers_sent($file, $line)) {
header($_SERVER['SERVER_PROTOCOL'] . " $this->statusCode " . $this->getStatusDescription());
foreach($this->headers as $header => $value) {
header("$header: $value", true, $this->statusCode);
}
} else {
// It's critical that these status codes are sent; we need to report a failure if not.
if($this->statusCode >= 300) {
user_error("Couldn't set response type to $this->statusCode because of output on line $line of $file", E_USER_WARNING);
}
}
// Only show error pages or generic "friendly" errors if the status code signifies
@ -269,14 +278,6 @@ class SS_HTTPResponse {
public function isFinished() {
return in_array($this->statusCode, array(301, 302, 401, 403));
}
/**
* Set content-length in bytes. Should be called right before {@link output()}.
*/
public function fixContentLength() {
// Use mbstring to avoid problems with mb_internal_encoding() and mbstring.func_overload
$this->headers['Content-Length'] = mb_strlen($this->body,'8bit');
}
}

View File

@ -88,7 +88,7 @@ class RequestHandler extends ViewableData {
* </code>
*
* Form getters count as URL actions as well, and should be included in allowed_actions.
* Form actions on the other handed (first argument to {@link FormAction()} shoudl NOT be included,
* Form actions on the other handed (first argument to {@link FormAction()} should NOT be included,
* these are handled separately through {@link Form->httpSubmission}. You can control access on form actions
* either by conditionally removing {@link FormAction} in the form construction,
* or by defining $allowed_actions in your {@link Form} class.
@ -145,100 +145,175 @@ class RequestHandler extends ViewableData {
$this->request = $request;
$this->setDataModel($model);
$match = $this->findAction($request);
// If nothing matches, return this object
if (!$match) return $this;
// Start to find what action to call. Start by using what findAction returned
$action = $match['action'];
// We used to put "handleAction" as the action on controllers, but (a) this could only be called when
// you had $Action in your rule, and (b) RequestHandler didn't have one. $Action is better
if ($action == 'handleAction') {
Deprecation::notice('3.2.0', 'Calling handleAction directly is deprecated - use $Action instead');
$action = '$Action';
}
// Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action',
if($action[0] == '$') {
$action = str_replace("-", "_", $request->latestParam(substr($action,1)));
}
if(!$action) {
if(isset($_REQUEST['debug_request'])) {
Debug::message("Action not set; using default action method name 'index'");
}
$action = "index";
} else if(!is_string($action)) {
user_error("Non-string method name: " . var_export($action, true), E_USER_ERROR);
}
$className = get_class($this);
if(!$this->hasAction($action)) {
return new SS_HTTPResponse("Action '$action' isn't available on class $className.", 404);
}
if(!$this->checkAccessAction($action) || in_array(strtolower($action), array('run', 'init'))) {
return new SS_HTTPResponse("Action '$action' isn't allowed on class $className.", 403);
}
try {
$result = $this->handleAction($request, $action);
}
catch (SS_HTTPResponse_Exception $e) {
return $e->getResponse();
}
catch(PermissionFailureException $e) {
$result = Security::permissionFailure(null, $e->getMessage());
}
if($result instanceof SS_HTTPResponse && $result->isError()) {
if(isset($_REQUEST['debug_request'])) Debug::message("Rule resulted in HTTP error; breaking");
return $result;
}
// If we return a RequestHandler, call handleRequest() on that, even if there is no more URL to
// parse. It might have its own handler. However, we only do this if we haven't just parsed an
// empty rule ourselves, to prevent infinite loops. Also prevent further handling of controller
// actions which return themselves to avoid infinite loops.
$matchedRuleWasEmpty = $request->isEmptyPattern($match['rule']);
$resultIsRequestHandler = is_object($result) && $result instanceof RequestHandler;
if($this !== $result && !$matchedRuleWasEmpty && $resultIsRequestHandler) {
$returnValue = $result->handleRequest($request, $model);
// Array results can be used to handle
if(is_array($returnValue)) $returnValue = $this->customise($returnValue);
return $returnValue;
// If we return some other data, and all the URL is parsed, then return that
} else if($request->allParsed()) {
return $result;
// But if we have more content on the URL and we don't know what to do with it, return an error.
} else {
return $this->httpError(404, "I can't handle sub-URLs of a $this->class object.");
}
return $this;
}
protected function findAction($request) {
$handlerClass = ($this->class) ? $this->class : get_class($this);
// We stop after RequestHandler; in other words, at ViewableData
while($handlerClass && $handlerClass != 'ViewableData') {
$urlHandlers = Config::inst()->get($handlerClass, 'url_handlers', Config::FIRST_SET);
$urlHandlers = Config::inst()->get($handlerClass, 'url_handlers', Config::UNINHERITED);
if($urlHandlers) foreach($urlHandlers as $rule => $action) {
if(isset($_REQUEST['debug_request'])) {
Debug::message("Testing '$rule' with '" . $request->remaining() . "' on $this->class");
}
if($params = $request->match($rule, true)) {
// Backwards compatible setting of url parameters, please use SS_HTTPRequest->latestParam() instead
//Director::setUrlParams($request->latestParams());
if($request->match($rule, true)) {
if(isset($_REQUEST['debug_request'])) {
Debug::message("Rule '$rule' matched to action '$action' on $this->class."
. " Latest request params: " . var_export($request->latestParams(), true));
Debug::message(
"Rule '$rule' matched to action '$action' on $this->class. ".
"Latest request params: " . var_export($request->latestParams(), true)
);
}
// Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action',
if($action[0] == '$') $action = $params[substr($action,1)];
if($this->checkAccessAction($action)) {
if(!$action) {
if(isset($_REQUEST['debug_request'])) {
Debug::message("Action not set; using default action method name 'index'");
}
$action = "index";
} else if(!is_string($action)) {
user_error("Non-string method name: " . var_export($action, true), E_USER_ERROR);
}
try {
if(!$this->hasMethod($action)) {
return $this->httpError(404, "Action '$action' isn't available on class "
. get_class($this) . ".");
}
$result = $this->$action($request);
} catch(SS_HTTPResponse_Exception $responseException) {
$result = $responseException->getResponse();
}
} else {
return $this->httpError(403, "Action '$action' isn't allowed on class " . get_class($this));
}
if($result instanceof SS_HTTPResponse && $result->isError()) {
if(isset($_REQUEST['debug_request'])) Debug::message("Rule resulted in HTTP error; breaking");
return $result;
}
// If we return a RequestHandler, call handleRequest() on that, even if there is no more URL to
// parse. It might have its own handler. However, we only do this if we haven't just parsed an
// empty rule ourselves, to prevent infinite loops. Also prevent further handling of controller
// actions which return themselves to avoid infinite loops.
if($this !== $result && !$request->isEmptyPattern($rule) && is_object($result)
&& $result instanceof RequestHandler) {
$returnValue = $result->handleRequest($request, $model);
// Array results can be used to handle
if(is_array($returnValue)) $returnValue = $this->customise($returnValue);
return $returnValue;
// If we return some other data, and all the URL is parsed, then return that
} else if($request->allParsed()) {
return $result;
// But if we have more content on the URL and we don't know what to do with it, return an error.
} else {
return $this->httpError(404, "I can't handle sub-URLs of a $this->class object.");
}
return $this;
return array('rule' => $rule, 'action' => $action);
}
}
$handlerClass = get_parent_class($handlerClass);
}
// If nothing matches, return this object
return $this;
}
/**
* Given a request, and an action name, call that action name on this RequestHandler
*
* Must not raise SS_HTTPResponse_Exceptions - instead it should return
*
* @param $request
* @param $action
* @return SS_HTTPResponse
*/
protected function handleAction($request, $action) {
$className = get_class($this);
if(!$this->hasMethod($action)) {
return new SS_HTTPResponse("Action '$action' isn't available on class $className.", 404);
}
$res = $this->extend('beforeCallActionHandler', $request, $action);
if ($res) return reset($res);
$actionRes = $this->$action($request);
$res = $this->extend('afterCallActionHandler', $request, $action);
if ($res) return reset($res);
return $actionRes;
}
/**
* Get a unified array of allowed actions on this controller (if such data is available) from both the controller
* ancestry and any extensions.
* Get a array of allowed actions defined on this controller,
* any parent classes or extensions.
*
* Caution: Since 3.1, allowed_actions definitions only apply
* to methods on the controller they're defined on,
* so it is recommended to use the $class argument
* when invoking this method.
*
* @param String $limitToClass
* @return array|null
*/
public function allowedActions() {
public function allowedActions($limitToClass = null) {
if($limitToClass) {
$actions = Config::inst()->get(
$limitToClass,
'allowed_actions',
Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES
);
} else {
$actions = Config::inst()->get(get_class($this), 'allowed_actions');
}
$actions = Config::inst()->get(get_class($this), 'allowed_actions');
if(is_array($actions)) {
if(array_key_exists('*', $actions)) {
Deprecation::notice(
'3.0',
'Wildcards (*) are no longer valid in $allowed_actions due their ambiguous '
. ' and potentially insecure behaviour. Please define all methods explicitly instead.'
);
}
if($actions) {
// convert all keys and values to lowercase to
// allow for easier comparison, unless it is a permission code
$actions = array_change_key_case($actions, CASE_LOWER);
@ -248,35 +323,48 @@ class RequestHandler extends ViewableData {
}
return $actions;
} else {
return null;
}
}
/**
* Checks if this request handler has a specific action (even if the current user cannot access it).
* Checks if this request handler has a specific action,
* even if the current user cannot access it.
* Includes class ancestry and extensions in the checks.
*
* @param string $action
* @return bool
*/
public function hasAction($action) {
if($action == 'index') return true;
// Don't allow access to any non-public methods (inspect instance plus all extensions)
$insts = array_merge(array($this), (array)$this->getExtensionInstances());
foreach($insts as $inst) {
if(!method_exists($inst, $action)) continue;
$r = new ReflectionClass(get_class($inst));
$m = $r->getMethod($action);
if(!$m || !$m->isPublic()) return false;
}
$action = strtolower($action);
$actions = $this->allowedActions();
// Check if the action is defined in the allowed actions as either a
// key or value. Note that if the action is numeric, then keys are not
// searched for actions to prevent actual array keys being recognised
// as actions.
// Check if the action is defined in the allowed actions of any ancestry class
// as either a key or value. Note that if the action is numeric, then keys are not
// searched for actions to prevent actual array keys being recognised as actions.
if(is_array($actions)) {
$isKey = !is_numeric($action) && array_key_exists($action, $actions);
$isValue = in_array($action, $actions, true);
$isWildcard = (in_array('*', $actions) && $this->checkAccessAction($action));
if($isKey || $isValue || $isWildcard) return true;
if($isKey || $isValue) return true;
}
if(!is_array($actions) || !$this->config()->get('allowed_actions',
Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES)) {
$actionsWithoutExtra = $this->config()->get(
'allowed_actions',
Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES
);
if(!is_array($actions) || !$actionsWithoutExtra) {
if($action != 'init' && $action != 'run' && method_exists($this, $action)) return true;
}
@ -289,60 +377,66 @@ class RequestHandler extends ViewableData {
*/
public function checkAccessAction($action) {
$actionOrigCasing = $action;
$action = strtolower($action);
$allowedActions = $this->allowedActions();
$action = strtolower($action);
if($allowedActions) {
// check for specific action rules first, and fall back to global rules defined by asterisk
foreach(array($action,'*') as $actionOrAll) {
// check if specific action is set
if(isset($allowedActions[$actionOrAll])) {
$test = $allowedActions[$actionOrAll];
if($test === true || $test === 1 || $test === '1') {
// Case 1: TRUE should always allow access
return true;
} elseif(substr($test, 0, 2) == '->') {
// Case 2: Determined by custom method with "->" prefix
list($method, $arguments) = Object::parse_class_spec(substr($test, 2));
return call_user_func_array(array($this, $method), $arguments);
} else {
// Case 3: Value is a permission code to check the current member against
return Permission::check($test);
}
} elseif((($key = array_search($actionOrAll, $allowedActions, true)) !== false) && is_numeric($key)) {
// Case 4: Allow numeric array notation (search for array value as action instead of key)
return true;
}
$isAllowed = false;
$isDefined = false;
if($this->hasMethod($actionOrigCasing) || !$action || $action == 'index') {
// Get actions for this specific class (without inheritance)
$definingClass = null;
$insts = array_merge(array($this), (array)$this->getExtensionInstances());
foreach($insts as $inst) {
if(!method_exists($inst, $action)) continue;
$r = new ReflectionClass(get_class($inst));
$m = $r->getMethod($actionOrigCasing);
$definingClass = $m->getDeclaringClass()->getName();
}
}
// If we get here an the action is 'index', then it hasn't been specified, which means that
// it should be allowed.
if($action == 'index' || empty($action)) return true;
if($allowedActions === null || !$this->config()->get('allowed_actions',
Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES)) {
// If no allowed_actions are provided, then we should only let through actions that aren't handled by
// magic methods we test this by calling the unmagic method_exists.
if(method_exists($this, $action)) {
// Disallow any methods which aren't defined on RequestHandler or subclasses
// (e.g. ViewableData->getSecurityID())
$r = new ReflectionClass(get_class($this));
if($r->hasMethod($actionOrigCasing)) {
$m = $r->getMethod($actionOrigCasing);
return ($m && is_subclass_of($m->getDeclaringClass()->getName(), 'RequestHandler'));
$allowedActions = $this->allowedActions($definingClass);
// check if specific action is set
if(isset($allowedActions[$action])) {
$isDefined = true;
$test = $allowedActions[$action];
if($test === true || $test === 1 || $test === '1') {
// TRUE should always allow access
$isAllowed = true;
} elseif(substr($test, 0, 2) == '->') {
// Determined by custom method with "->" prefix
list($method, $arguments) = Object::parse_class_spec(substr($test, 2));
$definingClassInst = Injector::inst()->get($definingClass);
$isAllowed = call_user_func_array(array($definingClassInst, $method), $arguments);
} else {
throw new Exception("method_exists() true but ReflectionClass can't find method - PHP is b0kred");
// Value is a permission code to check the current member against
$isAllowed = Permission::check($test);
}
} else if(!$this->hasMethod($action)){
// Return true so that a template can handle this action
return true;
} elseif(
is_array($allowedActions)
&& (($key = array_search($action, $allowedActions, true)) !== false)
&& is_numeric($key)
) {
// Allow numeric array notation (search for array value as action instead of key)
$isDefined = true;
$isAllowed = true;
} elseif(is_array($allowedActions) && !count($allowedActions)) {
// If defined as empty array, deny action
$isAllowed = false;
} elseif($allowedActions === null) {
// If undefined, allow action
$isAllowed = true;
}
// If we don't have a match in allowed_actions,
// whitelist the 'index' action as well as undefined actions.
if(!$isDefined && ($action == 'index' || empty($action))) {
$isAllowed = true;
}
} else {
// Doesn't have method, set to true so that a template can handle this action
$isAllowed = true;
}
return false;
return $isAllowed;
}
/**

View File

@ -415,7 +415,7 @@ class Session {
protected function recursivelyApply($data, &$dest) {
foreach($data as $k => $v) {
if(is_array($v)) {
if(!isset($dest[$k])) $dest[$k] = array();
if(!isset($dest[$k]) || !is_array($dest[$k])) $dest[$k] = array();
$this->recursivelyApply($v, $dest[$k]);
} else {
$dest[$k] = $v;
@ -440,8 +440,8 @@ class Session {
* @param type the type of message
*/
public static function setFormMessage($formname,$message,$type){
Session::set("FormInfo.$formname.message", $message);
Session::set("FormInfo.$formname.type", $type);
Session::set("FormInfo.$formname.formError.message", $message);
Session::set("FormInfo.$formname.formError.type", $type);
}
/**

View File

@ -692,6 +692,7 @@ class Injector {
* Register a service with an explicit name
*/
public function registerNamedService($name, $service) {
$this->specs[$name] = array('class' => get_class($service));
$this->serviceCache[$name] = $service;
$this->inject($service);
}
@ -773,6 +774,9 @@ class Injector {
if (isset($this->specs[$name])) {
$spec = $this->specs[$name];
$this->updateSpecConstructor($spec);
if ($constructorArgs) {
$spec['constructor'] = $constructorArgs;
}
return $this->instantiate($spec, $name);
}
}

View File

@ -437,7 +437,7 @@ abstract class Object {
if(func_num_args() > 1) {
Deprecation::notice('3.1.0', "Object::has_extension() deprecated. Call has_extension() on the class");
$class = func_get_arg(0);
$extension = func_get_arg(1);
$requiredExtension = func_get_arg(1);
}
$requiredExtension = strtolower($requiredExtension);

View File

@ -17,6 +17,8 @@ Used in side panels and action tabs
.backlink { padding-left: 12px; }
#Form_EditorToolbarMediaForm .ui-tabs-panel { padding-left: 0px; }
body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fieldset { padding: 16px; overflow: auto; background: #E2E2E2; }
body.cms.ss-uploadfield-edit-iframe span.readonly, .composite.ss-assetuploadfield .details fieldset span.readonly { font-style: italic; color: #777777; text-shadow: 0px 1px 0px #fff; }
body.cms.ss-uploadfield-edit-iframe .fieldholder-small label, .composite.ss-assetuploadfield .details fieldset .fieldholder-small label { margin-left: 0; }
@ -36,7 +38,7 @@ body.cms.ss-uploadfield-edit-iframe .fieldholder-small label, .composite.ss-asse
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item .info { position: relative; padding: 7px; overflow: hidden; background-color: #FFBE66; border: 1px solid #FF9300; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-preview { position: absolute; height: 30px; width: 40px; overflow: hidden; z-index: 1; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-preview .no-preview { display: block; height: 100%; width: 100%; background: url("../images/icons/document.png") 2px 0px no-repeat; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-info { position: relative; line-height: 30px; font-size: 18px; overflow: hidden; background-color: #5db4df; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #5db4df), color-stop(8%, #5db1dd), color-stop(50%, #439bcb), color-stop(54%, #3f99cd), color-stop(96%, #207db6), color-stop(100%, #1e7cba)); background-image: -webkit-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: -moz-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: -o-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-info { position: relative; line-height: 30px; font-size: 14px; overflow: hidden; background-color: #5db4df; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #5db4df), color-stop(8%, #5db1dd), color-stop(50%, #439bcb), color-stop(54%, #3f99cd), color-stop(96%, #207db6), color-stop(100%, #1e7cba)); background-image: -webkit-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: -moz-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: -o-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); }
.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info { background-color: #c11f1d; padding-right: 130px; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #c11f1d), color-stop(4%, #bf1d1b), color-stop(8%, #b71b1c), color-stop(15%, #b61e1d), color-stop(27%, #b11d1d), color-stop(31%, #ab1d1c), color-stop(42%, #a51b1b), color-stop(46%, #9f1b19), color-stop(50%, #9f1b19), color-stop(54%, #991c1a), color-stop(58%, #971a18), color-stop(62%, #911b1b), color-stop(65%, #911b1b), color-stop(88%, #7e1816), color-stop(92%, #771919), color-stop(100%, #731817)); background-image: -webkit-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: -moz-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: -o-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); }
.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info .ss-uploadfield-item-name { width: 100%; cursor: default; background: #bcb9b9; background: rgba(201, 198, 198, 0.9); }
.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info .ss-uploadfield-item-name .name { text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.7); }
@ -47,13 +49,13 @@ body.cms.ss-uploadfield-edit-iframe .fieldholder-small label, .composite.ss-asse
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name .ss-uploadfield-item-status.ui-state-error-text { max-width: 70%; position: absolute; right: 5px; text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.6); color: #cc0000; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name .ss-uploadfield-item-status.ui-state-warning-text { color: #b7a403; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name .ss-uploadfield-item-status.ui-state-success-text { color: #1f9433; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions { position: absolute; top: 0; right: 0; left: 0; z-index: 0; color: #f00; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions { position: absolute; top: 0; right: 0; left: 0; z-index: 0; color: #f00; font-size: 14px; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button { background: none; border: 0; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; text-shadow: none; color: white; float: right; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-delete { display: none; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel { -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; border-left: 1px solid rgba(255, 255, 255, 0.2); margin-top: 3px; cursor: pointer; opacity: 0.9; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel { -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; border-left: 1px solid rgba(255, 255, 255, 0.2); margin-top: 0px; cursor: pointer; opacity: 0.9; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel:hover { opacity: 1; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel .ui-icon { display: block; margin: 0; position: realtive; top: 4px; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit { opacity: 0.9; padding-top: 3px; padding-bottom: 0; height: 100%; -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel .ui-icon { display: block; margin: 0; position: realtive; top: 8px; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit { opacity: 0.9; padding-top: 1px; padding-bottom: 0; height: 100%; -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit.ui-state-hover { background: none; opacity: 1; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit.ui-state-hover span.toggle-details { opacity: 1; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit span.toggle-details { opacity: 0.9; margin-left: 3px; display: inline-block; width: 5px; height: 100%; cursor: pointer; }
@ -66,16 +68,17 @@ body.cms.ss-uploadfield-edit-iframe .fieldholder-small label, .composite.ss-asse
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-progress .ss-uploadfield-item-progressbarvalue { width: 0; background: #60b3dd url(../images/progressbar_blue.gif) repeat left center; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-editform { /* don't use display none, for it will break jQuery('iframe').contents().height() */ height: 0; overflow: hidden; clear: both; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-editform iframe { width: 100%; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info { float: left; margin: 34px 0 0; }
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info { margin: 15px 0px 0 20px; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info label { font-size: 16px; line-height: 30px; padding: 5px 16px; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info { float: left; margin: 10px 0 0; }
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info { margin: 10px 0px 0 20px; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info label { font-size: 18px; line-height: 30px; padding: 8px 16px; margin-right: 0px; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-fromcomputer { /*position: relative; */ overflow: hidden; display: block; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-uploador { float: left; font-weight: bold; font-size: 22px; padding: 0 20px; line-height: 70px; margin-top: 16px; display: none; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-fromcomputer .btn-icon-drive-upload-large { background: url(../images/drive-upload-large.png) no-repeat 0px -4px; width: 32px; height: 32px; margin-top: -12px; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-uploador { float: left; font-weight: bold; font-size: 22px; padding: 0 20px; line-height: 70px; margin-top: 4px; display: none; }
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-uploador { font-size: 18px; margin-top: 0; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone { margin-top: 9px; -webkit-border-radius: 13px; -moz-border-radius: 13px; -ms-border-radius: 13px; -o-border-radius: 13px; border-radius: 13px; -webkit-box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; -moz-box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; border: 2px dashed gray; background: #d4dbe0; display: none; height: 82px; width: 360px; float: left; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone { margin-top: 9px; -webkit-border-radius: 13px; -moz-border-radius: 13px; -ms-border-radius: 13px; -o-border-radius: 13px; border-radius: 13px; -webkit-box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; -moz-box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; border: 2px dashed gray; background: #d4dbe0; display: none; height: 54px; width: 360px; float: left; text-align: center; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone.active.hover { -webkit-box-shadow: rgba(255, 255, 255, 0.6) 0 0 3px 2px inset; -moz-box-shadow: rgba(255, 255, 255, 0.6) 0 0 3px 2px inset; box-shadow: rgba(255, 255, 255, 0.6) 0 0 3px 2px inset; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div { color: #5e5e5e; text-shadow: 0px 1px 0px #fff; background: url("../images/upload.png") 0 25px no-repeat; width: 230px; z-index: 1; padding: 20px 0 0; line-height: 25px; font-size: 25px; font-weight: bold; text-align: center; display: block; margin: 0 auto; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div span { display: block; font-size: 14px; z-index: -1; }
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone { margin-top: 3px; height: 56px; width: 277px; overflow: hidden; }
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div { background-position: 0 15px; }
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div span { height: 30px; font-size: 18px; line-height: 18px; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div { color: #5e5e5e; text-shadow: 0px 1px 0px #fff; background: url("../images/upload.png") 0 12px no-repeat; z-index: 1; padding: 20px 38px 0; line-height: 25px; font-size: 20px; font-weight: bold; display: inline-block; margin: 0 auto; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div span { display: block; font-size: 12px; z-index: -1; margin-top: -3px; }
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone { height: 54px; width: 277px; overflow: hidden; }
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div { background-position: 0 13px; padding-top: 22px; }
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div span { height: 38px; font-size: 18px; line-height: 18px; }

View File

@ -1,5 +1 @@
.datetime .middleColumn .middleColumn { margin: 0; padding: 0; clear: none; float: left; }
.datetime .date .middleColumn { width: 20em; }
.datetime .time .middleColumn { width: 10em; }

View File

@ -115,7 +115,7 @@ Used in side panels and action tabs
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination { padding-top: 1px; position: absolute; left: 50%; margin-left: -116px; z-index: 5; }
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination .pagination-page-number { color: white; text-shadow: 0px -1px 0 rgba(0, 0, 0, 0.2); }
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination .pagination-page-number input { width: 35px; height: 18px; margin-bottom: -6px; padding: 0px; border: 1px solid #899eab; border-bottom: 1px solid #a7b7c1; }
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination button { -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; border: none; width: 10px; margin: 0 10px; }
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination button { -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; border: none; width: 10px; margin: 0 10px; display: inline; float: none; }
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination button span { text-indent: -9999em; }
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination button.ss-gridfield-previouspage { background: url(../images/icons/pagination-arrows.png) no-repeat -23px 8px; }
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-pagination button.ss-gridfield-nextpage { background: url(../images/icons/pagination-arrows.png) no-repeat -47px 8px; }

View File

@ -223,6 +223,7 @@ class Debug {
public static function noticeHandler($errno, $errstr, $errfile, $errline, $errcontext) {
if(error_reporting() == 0) return;
ini_set('display_errors', 0);
// Send out the error details to the logger for writing
SS_Log::log(
@ -237,7 +238,9 @@ class Debug {
);
if(Director::isDev()) {
self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Notice");
return self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Notice");
} else {
return false;
}
}
@ -252,8 +255,10 @@ class Debug {
*/
public static function warningHandler($errno, $errstr, $errfile, $errline, $errcontext) {
if(error_reporting() == 0) return;
ini_set('display_errors', 0);
if(self::$send_warnings_to) {
self::emailError(self::$send_warnings_to, $errno, $errstr, $errfile, $errline, $errcontext, "Warning");
return self::emailError(self::$send_warnings_to, $errno, $errstr, $errfile, $errline, $errcontext, "Warning");
}
// Send out the error details to the logger for writing
@ -273,8 +278,10 @@ class Debug {
}
if(Director::isDev()) {
self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Warning");
}
return self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Warning");
} else {
return false;
}
}
/**
@ -289,6 +296,8 @@ class Debug {
* @param unknown_type $errcontext
*/
public static function fatalHandler($errno, $errstr, $errfile, $errline, $errcontext) {
ini_set('display_errors', 0);
if(self::$send_errors_to) {
self::emailError(self::$send_errors_to, $errno, $errstr, $errfile, $errline, $errcontext, "Error");
}
@ -310,11 +319,10 @@ class Debug {
}
if(Director::isDev() || Director::is_cli()) {
self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Error");
return self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Error");
} else {
self::friendlyError();
return self::friendlyError();
}
exit(1);
}
/**
@ -373,6 +381,7 @@ class Debug {
$renderer->writeFooter();
}
}
return false;
}
/**
@ -497,7 +506,7 @@ class Debug {
$_SESSION['Security']['Message']['type'] = 'warning';
$_SESSION['BackURL'] = $_SERVER['REQUEST_URI'];
header($_SERVER['SERVER_PROTOCOL'] . " 302 Found");
header("Location: " . Director::baseURL() . "Security/login");
header("Location: " . Director::baseURL() . Security::login_url());
die();
}
}
@ -524,7 +533,7 @@ function exceptionHandler($exception) {
$file = $exception->getFile();
$line = $exception->getLine();
$context = $exception->getTrace();
Debug::fatalHandler($errno, $message, $file, $line, $context);
return Debug::fatalHandler($errno, $message, $file, $line, $context);
}
/**
@ -543,21 +552,18 @@ function errorHandler($errno, $errstr, $errfile, $errline) {
case E_ERROR:
case E_CORE_ERROR:
case E_USER_ERROR:
Debug::fatalHandler($errno, $errstr, $errfile, $errline, null);
break;
return Debug::fatalHandler($errno, $errstr, $errfile, $errline, debug_backtrace());
case E_WARNING:
case E_CORE_WARNING:
case E_USER_WARNING:
Debug::warningHandler($errno, $errstr, $errfile, $errline, null);
break;
return Debug::warningHandler($errno, $errstr, $errfile, $errline, debug_backtrace());
case E_NOTICE:
case E_USER_NOTICE:
case E_DEPRECATED:
case E_USER_DEPRECATED:
case E_STRICT:
Debug::noticeHandler($errno, $errstr, $errfile, $errline, null);
break;
return Debug::noticeHandler($errno, $errstr, $errfile, $errline, debug_backtrace());
}
}

View File

@ -41,11 +41,8 @@ class SS_LogErrorEmailFormatter implements Zend_Log_Formatter_Interface {
$data .= "<p style=\"color: white; background-color: $colour; margin: 0\">"
. "[$errorType] $errstr<br />$errfile:$errline\n<br />\n<br />\n</p>\n";
// Get a backtrace, filtering out debug method calls
$data .= SS_Backtrace::backtrace(true, false, array(
'SS_LogErrorEmailFormatter->format',
'SS_LogEmailWriter->_write'
));
// Render the provided backtrace
$data .= SS_Backtrace::get_rendered_backtrace($errcontext);
// Compile extra data
$blacklist = array('message', 'timestamp', 'priority', 'priorityName');

View File

@ -205,7 +205,15 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$className = get_class($this);
$fixtureFile = eval("return {$className}::\$fixture_file;");
$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
// Set up email
$this->originalMailer = Email::mailer();
$this->mailer = new TestMailer();
Email::set_mailer($this->mailer);
Config::inst()->remove('Email', 'send_all_emails_to');
Email::send_all_emails_to(null);
// Todo: this could be a special test model
$this->model = DataModel::inst();
@ -259,12 +267,6 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$this->logInWithPermission("ADMIN");
}
// Set up email
$this->originalMailer = Email::mailer();
$this->mailer = new TestMailer();
Email::set_mailer($this->mailer);
Email::send_all_emails_to(null);
// Preserve memory settings
$this->originalMemoryLimit = ini_get('memory_limit');

View File

@ -43,7 +43,7 @@ class TaskRunner extends Controller {
echo "<ul>";
foreach($tasks as $task) {
echo "<li><p>";
echo "<a href=\"{$base}dev/tasks/" . $task['class'] . "\">" . $task['title'] . "</a><br />";
echo "<a href=\"{$base}dev/tasks/" . $task['segment'] . "\">" . $task['title'] . "</a><br />";
echo "<span class=\"description\">" . $task['description'] . "</span>";
echo "</p></li>\n";
}
@ -54,28 +54,41 @@ class TaskRunner extends Controller {
} else {
echo "SILVERSTRIPE DEVELOPMENT TOOLS: Tasks\n--------------------------\n\n";
foreach($tasks as $task) {
echo " * $task[title]: sake dev/tasks/" . $task['class'] . "\n";
echo " * $task[title]: sake dev/tasks/" . $task['segment'] . "\n";
}
}
}
public function runTask($request) {
$taskName = $request->param('TaskName');
if (class_exists($taskName) && is_subclass_of($taskName, 'BuildTask')) {
$title = singleton($taskName)->getTitle();
if(Director::is_cli()) echo "Running task '$title'...\n\n";
elseif(!Director::is_ajax()) echo "<h1>Running task '$title'...</h1>\n";
$name = $request->param('TaskName');
$tasks = $this->getTasks();
$task = new $taskName();
if ($task->isEnabled()) $task->run($request);
else echo "<p>{$title} is disabled</p>";
} else {
echo "Build task '$taskName' not found.";
if(class_exists($taskName)) echo " It isn't a subclass of BuildTask.";
echo "\n";
$title = function ($content) {
printf(Director::is_cli() ? "%s\n\n" : '<h1>%s</h1>', $content);
};
$message = function ($content) {
printf(Director::is_cli() ? "%s\n" : '<p>%s</p>', $content);
};
foreach ($tasks as $task) {
if ($task['segment'] == $name) {
$inst = Injector::inst()->create($task['class']);
$title(sprintf('Running Task %s', $inst->getTitle()));
if (!$inst->isEnabled()) {
$message('The task is disabled');
return;
}
$inst->run($request);
return;
}
}
$message(sprintf('The build task "%s" could not be found', $name));
}
/**
* @return array Array of associative arrays for each task (Keys: 'class', 'title', 'description')
*/
@ -95,13 +108,14 @@ class TaskRunner extends Controller {
$availableTasks[] = array(
'class' => $class,
'title' => singleton($class)->getTitle(),
'segment' => str_replace('\\', '-', $class),
'description' => $desc,
);
}
return $availableTasks;
}
}

View File

@ -96,8 +96,12 @@ class TestSession {
$form->setField(new SimpleByName($k), $v);
}
if($button) $submission = $form->submitButton(new SimpleByName($button));
else $submission = $form->submit();
if($button) {
$submission = $form->submitButton(new SimpleByName($button));
if(!$submission) throw new Exception("Can't find button '$button' to submit as part of test.");
} else {
$submission = $form->submit();
}
$url = Director::makeRelative($form->getAction()->asString());
@ -137,6 +141,15 @@ class TestSession {
return $this->lastResponse;
}
/**
* Return the fake HTTP_REFERER; set each time get() or post() is called.
*
* @return string
*/
public function lastUrl() {
return $this->lastUrl;
}
/**
* Get the most recent response's content
*/

View File

@ -1277,11 +1277,20 @@ HTML;
Deny from all
</Files>
# This denies access to all yml files, since developers might include sensitive
# information in them. See the docs for work-arounds to serve some yaml files
<Files *.yml>
Order allow,deny
Deny from all
</Files>
ErrorDocument 404 /assets/error-404.html
ErrorDocument 500 /assets/error-500.html
<IfModule mod_alias.c>
RedirectMatch 403 /silverstripe-cache(/|$)
RedirectMatch 403 /vendor(/|$)
RedirectMatch 403 /composer\.(json|lock)
</IfModule>
<IfModule mod_rewrite.c>

View File

@ -116,7 +116,7 @@ Any arrays you pass as values to `update()` will be automatically merged. To rep
Note the different options for the third parameter of `get()`:
* `Config::INHERITED` will only get the configuration set for the specific class, not any of it's parents.
* `Config::UNINHERITED` will only get the configuration set for the specific class, not any of it's parents.
* `Config::FIRST_SET` will inherit configuration from parents, but stop on the first class that actually provides a value.
* `Config::EXCLUDE_EXTRA_SOURCES` will not use additional static sources (such as those defined on extensions)

View File

@ -1,12 +1,242 @@
# 3.0.4
# 3.0.4 (2013-02-19)
## Overview
* Security: Undefined or empty `$allowed_actions` overrides parent definitions
* Security: Information leakage through web access on YAML configuration files
* Security: Information leakage through web access on composer files
* Security: Require ADMIN permissions for `?showtemplate=1`
* Security: Reflected XSS in custom date/time formats in admin/security
* Security: Stored XSS in the "New Group" dialog
* Security: Reflected XSS in CMS status messages
* API: More restrictive `$allowed_actions` checks for `Controller` when used with `Extension`
* Changed `dev/tests/setdb` and `dev/tests/startsession` from session to cookie storage.
## Details
### Security: Undefined or empty `$allowed_actions` overrides parent definitions
Severity: Important
Description: `Controller` (and subclasses) failed to enforce `$allowed_action` restrictions
on parent classes if a child class didn't have it explicitly defined, or it is set to an empty array.
Since this is the default configuration on `Page_Controller`, most SilverStripe installations
will be affected.
Impact: Depends on the used controller code. For any method with public visibility,
the flaw can expose the return value of the method (unless it fails due to wrong arguments).
It can also lead to unauthorized or unintended execution of logic, e.g. modifying the
state of a database record.
Fix: Apply 3.0.4 update. In addition, we strongly recommend to define `$allowed_actions`
on all controller classes to ensure the intentions are clearly communicated.
Read more about `$allowed_actions` in our "[controller](/topics/controller/#access-control)"
docs.
Reporter: Zann St Pierre
### Security: Information exposure through web access on YAML configuration files
Severity: Moderate
Description: YAML files are used to configure the SilverStripe application
since its 3.0 release. These files can contain sensitive values such as database
and API credentials. By default, the installer still stores database credentials
in `_config.php` files which are safe from web access. So this only concerns
configuration values added in your own project, or a third party module.
Resolution: Update your `.htaccess` file (for Apache), or your `web.config` file (for IIS)
with the new files from the project root, and reapply any customizations you've made.
Follow the [general upgrade instructions](/installation/upgrading).
The [nginx installation instructions](/installation/nginx)
have also been updated to reflect those changes.
### Security: Information exposure through web access on composer files
Severity: Low
Description: [Composer](http://getcomposer.org) is a dependency management
tool which can optionally be used to install SilverStripe. The `composer.json`
and `composer.lock` files are required for its operation, so they are included
in the standard release since 3.0.2. These files contain information on the installed
versions of core and thirdparty modules, which could be used to target specific
versions of SilverStripe.
Resolution: Update your `.htaccess` file (for Apache), or your `web.config` file (for IIS)
with the new files from the project root, and reapply any customizations you've made.
Follow the [general upgrade instructions](/installation/upgrading).
The [nginx installation instructions](/installation/nginx)
have also been updated to reflect those changes.
### Security: Require ADMIN permissions for `?showtemplate=1`
Severity: Low
Description: Avoids information leakage of compiled template data,
which might expose some of the internal template logic.
### Security: Reflected XSS in custom date/time formats in admin/security
Severity: Low
Prerequisite: An attacker must have access to the admin interface.
Description: When creating a new user on the security page
(Security->New User) within the admin interface, the user input
is not properly validated and not encoded. A reflected XSS is
possible within the `DateFormat_custom` and `TimeFormat_custom` fields.
Credits: Andreas Hunkeler (Compass Security AG, http://www.csnc.ch)
### Security: Stored XSS in the "New Group" dialog
Severity: Low
Prerequisite: An attacker must have access to the admin interface.
Description: There is a stored XSS vulnerability on the "group" tab on the
security page in the admin interface
(Security -> Groups -> New Group). It's possible to store a
XSS within the group name. Everywhere where these group names
are used, the XSS is executed. E.g. "New User" or "New Group".
Credits: Andreas Hunkeler (Compass Security AG, http://www.csnc.ch)
### Security: XSS in CMS status messages
Severity: Low
Prerequisite: An attacker must have access to the admin interface.
Description: Any data returned to CMS status messages (Growl-style popovers on top right)
was not escaped, allowing XSS e.g. when publishing a page with
a specifically crafted "Title" field.
Credits: Andreas Hunkeler (Compass Security AG, http://www.csnc.ch)
### API: More restrictive `$allowed_actions` checks for `Controller` when used with `Extension`
Controllers which are extended with `$allowed_actions` (through an `Extension`)
now deny access to methods defined on the controller, unless this class also has them in its own
`$allowed_actions` definition.
## Upgrading
* If you are using `dev/tests/setdb` and `dev/tests/startsession`,
you'll need to configure a secure token in order to encrypt the cookie value:
Simply run `sake dev/generatesecuretoken` and add the resulting code to your `mysite/_config.php`.
Note that this functionality now requires the PHP `mcrypt` extension.
Note that this functionality now requires the PHP `mcrypt` extension.
## Changelog
### API Changes
* 2013-02-15 [2352317](https://github.com/silverstripe/silverstripe-installer/commit/2352317) Filter composer files in IIS and Apache rules (fixes #8011) (Ingo Schommer)
* 2013-02-12 [d969e29](https://github.com/silverstripe/sapphire/commit/d969e29) Require ADMIN for ?showtemplate=1 (Ingo Schommer)
* 2013-02-12 [45c68d6](https://github.com/silverstripe/sapphire/commit/45c68d6) Require ADMIN for ?showtemplate=1 (Ingo Schommer)
* 2013-01-23 [c69381c](https://github.com/silverstripe/sapphire/commit/c69381c) Remove Content-Length setting from HTTPResponse (fixes #8010) (Ingo Schommer)
* 2012-12-11 [10d447e](https://github.com/silverstripe/silverstripe-installer/commit/10d447e) Removed 'make getallmodules', use composer instead (Ingo Schommer)
* 2012-12-10 [efa9ff9](https://github.com/silverstripe/sapphire/commit/efa9ff9) Queries added by DataList::addInnerJoin() and DataList::leftJoin() come after the base joins, not before. (stojg)
* 2012-12-06 [c6b1d4a](https://github.com/silverstripe/sapphire/commit/c6b1d4a) Storing alternative DB name in cookie rather than session (Ingo Schommer)
* 2012-11-29 [f49f1ff](https://github.com/silverstripe/sapphire/commit/f49f1ff) Rename Transliterator to SS_Transliterator to remove conflict with Intl extension (Simon Welsh)
* 2012-11-19 [96e56a8](https://github.com/silverstripe/silverstripe-installer/commit/96e56a8) Removed 'new-project' command (Ingo Schommer)
* 2012-11-19 [8b68644](https://github.com/silverstripe/silverstripe-installer/commit/8b68644) Moved build tools to new silverstripe-buildtools module (Ingo Schommer)
* 2012-11-09 [22095da](https://github.com/silverstripe/sapphire/commit/22095da) Hash autologin tokens before storing in the database. (Mateusz Uzdowski)
* 2011-03-16 [d8bfc0b](https://github.com/silverstripe/sapphire/commit/d8bfc0b) Added Security::set_login_url() so that you can define an alternative log-in page if you have made one yourself. (Sam Minnee)
* 2011-03-16 [f546979](https://github.com/silverstripe/sapphire/commit/f546979) Add a PermissionFailureException that can be thrown to trigger a log-in. (Sam Minnee)
### Features and Enhancements
* 2013-02-05 [f0621cd](https://github.com/silverstripe/sapphire/commit/f0621cd) Added ability to query size of Varchar (Daniel Hensby)
* 2013-02-01 [119d8aa](https://github.com/silverstripe/silverstripe-cms/commit/119d8aa) Do not display SilverStripeNavigator_CMSLink when in a LeftAndMain extension not just CMSMain extensions (UndefinedOffset)
* 2013-01-11 [5b450f7](https://github.com/silverstripe/sapphire/commit/5b450f7) Added replaceExistingFile setting for UploadField. (Sam Minnee)
* 2013-01-11 [cc7318f](https://github.com/silverstripe/sapphire/commit/cc7318f) Added canAttachExisting config option for UploadField. (Sam Minnee)
* 2013-01-10 [5e6f5f9](https://github.com/silverstripe/sapphire/commit/5e6f5f9) Allow configuration of send_all_emails_to, ccs_all_emails_to, and bcc_all_emails_to via the config system. (Sam Minnee)
* 2013-01-09 [67c5db3](https://github.com/silverstripe/sapphire/commit/67c5db3) Global default config for UploadField (Ingo Schommer)
* 2013-01-09 [2dfd427](https://github.com/silverstripe/sapphire/commit/2dfd427) Restrict upload abilities in UploadField (Ingo Schommer)
* 2013-01-07 [abbee41](https://github.com/silverstripe/sapphire/commit/abbee41) Add ReadonlyField::setIncludeHiddenField() (Sam Minnee)
* 2012-12-07 [e8fbfc0](https://github.com/silverstripe/sapphire/commit/e8fbfc0) FixtureFactory separated out from YamlFixture (Ingo Schommer)
* 2012-11-15 [e07ae20](https://github.com/silverstripe/silverstripe-installer/commit/e07ae20) Added "phing phpunit" target (Ingo Schommer)
* 2012-11-09 [32f829d](https://github.com/silverstripe/sapphire/commit/32f829d) Support for Behat tests, and initial set of tests (Ingo Schommer)
* 2012-11-09 [9841d5b](https://github.com/silverstripe/silverstripe-cms/commit/9841d5b) Added Behat tests (Ingo Schommer)
* 2012-10-24 [ea2dc9d](https://github.com/silverstripe/sapphire/commit/ea2dc9d) Add ability to change URL for SS logo in CMS Menu (Loz Calver)
* 2012-10-16 [c4dde90](https://github.com/silverstripe/sapphire/commit/c4dde90) Allow hashes to be passed as ArrayList items; the will be turned into ArrayData objects. (Sam Minnee)
* 2011-09-29 [2916f20](https://github.com/silverstripe/sapphire/commit/2916f20) Improve HTTP caching logic to automatically disable caching for requests that use the session. (Hamish Friedlander)
* 2011-03-02 [c3a3ff4](https://github.com/silverstripe/sapphire/commit/c3a3ff4) Added Email::send_all_emails_from() setting. (Sam Minnee)
### Bugfixes
* 2013-02-17 [ede3813](https://github.com/silverstripe/sapphire/commit/ede3813) Secure composer files from web access (fixes #8011) (Ingo Schommer)
* 2013-02-17 [e21bd49](https://github.com/silverstripe/sapphire/commit/e21bd49) TimeField respects user choice (fixes #8260) (Ingo Schommer)
* 2013-02-07 [79eacb2](https://github.com/silverstripe/sapphire/commit/79eacb2) Group-&gt;canEdit() correct non-admin checks (fixes #8250) (Ingo Schommer)
* 2013-02-04 [857d8bb](https://github.com/silverstripe/sapphire/commit/857d8bb) Don't escape values on TreeDropdownField readonly views (Ingo Schommer)
* 2013-02-04 [97fbfd3](https://github.com/silverstripe/silverstripe-cms/commit/97fbfd3) Respect escaping rules on readonly fields in CMS history view (Ingo Schommer)
* 2013-02-04 [e56a78b](https://github.com/silverstripe/silverstripe-cms/commit/e56a78b) updateCMSFields not accepting var by reference (Michael Andrewartha)
* 2013-02-04 [866bb07](https://github.com/silverstripe/sapphire/commit/866bb07) validate doesn't take var by reference (Michael Andrewartha)
* 2013-02-04 [1960df8](https://github.com/silverstripe/sapphire/commit/1960df8) Strict error warnings on DataExtension (Michael Andrewartha)
* 2013-01-31 [1bb1090](https://github.com/silverstripe/sapphire/commit/1bb1090) Node updates in IE without non-object error (Ingo Schommer)
* 2013-01-30 [c9f728f](https://github.com/silverstripe/sapphire/commit/c9f728f) Only check the remember token if a user exists (Simon Welsh)
* 2013-01-24 [f574979](https://github.com/silverstripe/sapphire/commit/f574979) Exception handling and email notification mechanism now correctly considers the stacktrace as provided by the exceptionHandler function, instead of attempting to perform a debug_backtrace further down the reporting chain (which ends up generating an unnecessarily nested stacktrace). Debug was cleaned up so that errorHandler and exceptionHandler both act consistently. As a result, the LogErrorEmailFormatter class could be simplified. (Damian Mooyman)
* 2013-01-23 [45eb0f9](https://github.com/silverstripe/sapphire/commit/45eb0f9) PHPUnit latest not working with composer installed builds (Hamish Friedlander)
* 2013-01-21 [5d37d55](https://github.com/silverstripe/sapphire/commit/5d37d55) Form session message clearing regression (Ingo Schommer)
* 2013-01-18 [0c9b216](https://github.com/silverstripe/sapphire/commit/0c9b216) Escape the -f argument passed to mail() (Sam Minnee)
* 2013-01-17 [e74ec57](https://github.com/silverstripe/sapphire/commit/e74ec57) Permission checkbox display on members (fixes #8193) (Ingo Schommer)
* 2013-01-15 [a70df3e](https://github.com/silverstripe/sapphire/commit/a70df3e) PaginatedList deprecated method was calling non-existent method (Jeremy Thomerson)
* 2013-01-15 [014f541](https://github.com/silverstripe/sapphire/commit/014f541) Regression in Form-&gt;clearMessage() (fixes #8186) (Ingo Schommer)
* 2013-01-15 [64d3a3d](https://github.com/silverstripe/sapphire/commit/64d3a3d) Don't double unescape URLs in history.js (fixes #8170) (Ingo Schommer)
* 2013-01-15 [420c639](https://github.com/silverstripe/sapphire/commit/420c639) Properly show link for showing and hiding class spec in model admin (jean)
* 2013-01-15 [f06ba70](https://github.com/silverstripe/sapphire/commit/f06ba70) Undefined `$allowed_actions` overrides parent definitions, stricter handling of $allowed_actions on Extension (Ingo Schommer)
* 2013-01-11 [e020c7b](https://github.com/silverstripe/sapphire/commit/e020c7b) doSave() and doDelete() should use translated singular name (uniun)
* 2013-01-11 [f8758ba](https://github.com/silverstripe/sapphire/commit/f8758ba) Fixed margins so that margin is displayed between preview images and their title. (Sam Minnee)
* 2013-01-11 [2fdd9a3](https://github.com/silverstripe/sapphire/commit/2fdd9a3) Allow images attached to UploadFields to be unlinked without File::canEdit() or File::canDelete() permission. (Sam Minnee)
* 2013-01-11 [f4efaee](https://github.com/silverstripe/sapphire/commit/f4efaee) Fix DataObject::get_one() when the classname is passed with improper casing. (Sam Minnee)
* 2013-01-06 [30096ee](https://github.com/silverstripe/sapphire/commit/30096ee) Keep Member.PasswordEncryption setting on empty passwords (Ingo Schommer)
* 2013-01-04 [f8bbc0a](https://github.com/silverstripe/sapphire/commit/f8bbc0a) Escape HTML in DropdownField and ListboxField (Ingo Schommer)
* 2013-01-04 [604ede3](https://github.com/silverstripe/sapphire/commit/604ede3) Escape HTML in CMS status messages (Ingo Schommer)
* 2013-01-04 [7bb0bbf](https://github.com/silverstripe/sapphire/commit/7bb0bbf) Fixed XSS in admin/security and "My Profile" forms (Ingo Schommer)
* 2012-12-21 [f0f83b2](https://github.com/silverstripe/sapphire/commit/f0f83b2) Graceful handling of sprintf with too few params in i18n::_t() (Ingo Schommer)
* 2012-12-19 [22efd38](https://github.com/silverstripe/sapphire/commit/22efd38) Calling DataObject::relField() on a object with an empty relation list (Stig Lindqvist)
* 2012-12-18 [6aba24b](https://github.com/silverstripe/sapphire/commit/6aba24b) removeRequiredField() should use array_splice() instead of unset() (uniun)
* 2012-12-18 [d5a1c3d](https://github.com/silverstripe/sapphire/commit/d5a1c3d) SS has problems handling + in URLs. Filter them out. (Mateusz Uzdowski)
* 2012-12-14 [55b611d](https://github.com/silverstripe/sapphire/commit/55b611d) Hardcoded project name in include_by_locale() (uniun)
* 2012-12-13 [d42c004](https://github.com/silverstripe/silverstripe-cms/commit/d42c004) Fixed pagination functionality on root assets folder (Niklas Forsdahl)
* 2012-12-12 [639cc02](https://github.com/silverstripe/sapphire/commit/639cc02) Fix insert media form inserting images from other UploadFields (fixes #8051) (Loz Calver)
* 2012-12-11 [f431b35](https://github.com/silverstripe/sapphire/commit/f431b35) Confirmed Password Field now copies attributes to child fields. (Justin Martin)
* 2012-12-07 [4f63f91](https://github.com/silverstripe/sapphire/commit/4f63f91) Fixed issue with convertServiceProperty (Marcus Nyeholt)
* 2012-12-06 [1a4eaaa](https://github.com/silverstripe/sapphire/commit/1a4eaaa) Ensure has length before using string index access. (Simon Elvery)
* 2012-12-05 [205ee42](https://github.com/silverstripe/sapphire/commit/205ee42) Make sure a message is set on ValidationException objects. (Simon Elvery)
* 2012-12-05 [c0751df](https://github.com/silverstripe/silverstripe-cms/commit/c0751df) Remove handwritten SQL and use the ORM. (Mateusz Uzdowski)
* 2012-12-04 [0be51a9](https://github.com/silverstripe/sapphire/commit/0be51a9) Fix ModelAdmin search (fixes #8052) (Ingo Schommer)
* 2012-12-04 [1a4b245](https://github.com/silverstripe/sapphire/commit/1a4b245) Fix rewriteHashlinks in TabSet (Marcus Nyeholt)
* 2012-12-04 [3478813](https://github.com/silverstripe/sapphire/commit/3478813) Rewrite hashlinks failing on empty a tags (Marcus Nyeholt)
* 2012-11-26 [40a1a35](https://github.com/silverstripe/silverstripe-cms/commit/40a1a35) Namespaces for CmsFormsContext and CmsUiContext are wrong (Kirk Mayo)
* 2012-11-23 [4310718](https://github.com/silverstripe/sapphire/commit/4310718) Insert Media, inserts all previous media (fixes #7545) (UndefinedOffset)
* 2012-11-23 [453d04e](https://github.com/silverstripe/sapphire/commit/453d04e) Reset DataObject caches in SapphireTest-&gt;resetDBSchema() (Ingo Schommer)
* 2012-11-23 [a3cd7dd](https://github.com/silverstripe/sapphire/commit/a3cd7dd) Force SapphireTest schema reset for extension changes (Ingo Schommer)
* 2012-11-21 [41aec54](https://github.com/silverstripe/silverstripe-cms/commit/41aec54) Consistently use FormResponse in CMS JavaScript (fixes #8036) (Ingo Schommer)
* 2012-11-20 [8f89aa9](https://github.com/silverstripe/sapphire/commit/8f89aa9) only call filemtime if file exists (Sander van Dragt)
* 2012-11-16 [76c63fe](https://github.com/silverstripe/sapphire/commit/76c63fe) Fixed issue with SQLQuery::lastRow crashing on empty set. Added test cases for lastRow and firstRow. (Damian Mooyman)
* 2012-11-15 [c6fcb08](https://github.com/silverstripe/sapphire/commit/c6fcb08) Video embed from Add Media Feature no longer works (open #8033) (stojg)
* 2012-11-14 [91e48b8](https://github.com/silverstripe/silverstripe-cms/commit/91e48b8) Provide fallback text for translations. (Simon Elvery)
* 2012-11-12 [2657a27](https://github.com/silverstripe/sapphire/commit/2657a27) Adjust the handler to jQuery UI 1.9 API change. (Mateusz Uzdowski)
* 2012-11-12 [b6017a7](https://github.com/silverstripe/sapphire/commit/b6017a7) ArrayList now discards keys of the array passed in and keeps the numerically indexed array sequential. This fixes FirstLast and EvenOdd in templates, and makes ArrayList more consistent, as several methods already discarded the keys. (Andrew O'Neil)
* 2012-11-12 [d58b23d](https://github.com/silverstripe/silverstripe-cms/commit/d58b23d) AssetAdmin filter array indices (fixes #8014) (Kirk Mayo)
* 2012-11-09 [434759c](https://github.com/silverstripe/sapphire/commit/434759c) Correct redirection URL on deletion in GridFieldDetailForm (Ingo Schommer)
* 2012-11-08 [6882635](https://github.com/silverstripe/sapphire/commit/6882635) Fixing non-object on file upload (Sean Harvey)
* 2012-11-08 [6a69a2f](https://github.com/silverstripe/silverstripe-cms/commit/6a69a2f) Ensure required lang and css are loaded when using SiteTreeURLSegmentField (Simon Elvery)
* 2012-02-09 [c048a01](https://github.com/silverstripe/sapphire/commit/c048a01) Avoid infinite redirection when logging out and when showing a custom login page after displaying the draft version of a page. (jean)
* 2011-12-12 [1e1df8c](https://github.com/silverstripe/sapphire/commit/1e1df8c) Improved detection of empty HTMLText fields. (Sam Minnee)
* 2011-09-30 [f41a7d8](https://github.com/silverstripe/sapphire/commit/f41a7d8) Fix issue with not being able to log out on Chrome when caching enabled because of Chrome bug (Hamish Friedlander)
* 2011-09-01 [9a2ba48](https://github.com/silverstripe/sapphire/commit/9a2ba48) Made CSRF-error wording friendlier. (Sam Minnee)
* 2011-08-31 [729bcc9](https://github.com/silverstripe/sapphire/commit/729bcc9) Don't clear form messages unless forTemplate() is actually called. BUGFIX: Clear session-stored form data as well as form error message. (Sam Minnee)
* 2011-08-18 [5f9348b](https://github.com/silverstripe/sapphire/commit/5f9348b) Ensure that Security views respect redirections triggered by Page_Controller::init() (Sam Minnee)
* 2011-07-07 [b114aa2](https://github.com/silverstripe/sapphire/commit/b114aa2) Added X-Forwarded-Protocol and User-Agent to Vary header. (Sam Minnee)
* 2011-05-26 [55f3ec1](https://github.com/silverstripe/sapphire/commit/55f3ec1) Added error message fields to default search form (Jean-Fabien)
* 2011-05-23 [7026a48](https://github.com/silverstripe/sapphire/commit/7026a48) for date manipulation use the SS_Datetime::now, otherwise it does not respect the mock date. (Mateusz Uzdowski)
* 2011-05-21 [b7a1db7](https://github.com/silverstripe/sapphire/commit/b7a1db7) Set up the test mailer before loading the fixture, in case fixture-creation causes emails to be generated. (Sam Minnee)
* 2011-04-29 [47e037e](https://github.com/silverstripe/sapphire/commit/47e037e) Removed notice-level error after forms w/ required fields are made readonly. (Sam Minnee)
* 2011-04-20 [33a1fc7](https://github.com/silverstripe/sapphire/commit/33a1fc7) Fixed operation of inlined images in Mailer, when no inlined images actually attached. (Carlos Barberis)
* 2011-04-18 [f8206d1](https://github.com/silverstripe/sapphire/commit/f8206d1) Prevent notice-level error in Session code when non-array is turned into an array. (Sam Minnee)
* 2011-03-15 [6fcbad1](https://github.com/silverstripe/sapphire/commit/6fcbad1) Updated SilverStripe error handler so that log_errors still works. (Sam Minnee)
* 2011-03-11 [82988d4](https://github.com/silverstripe/sapphire/commit/82988d4) Better error message when 401 response is corrupted. (Sam Minnee)

View File

@ -19,6 +19,9 @@
* Behaviour testing support through [Behat](http://behat.org), with CMS test coverage
(see the [SilverStripe Behat Extension]() for details)
* Removed legacy table APIs (e.g. `TableListField`), use GridField instead
* Deny URL access if `Controller::$allowed_actions` is undefined
* Removed support for "*" rules in `Controller::$allowed_actions`
* Removed support for overriding rules on parent classes through `Controller::$allowed_actions`
* Editing of relation table data (`$many_many_extraFields`) in `GridField`
* Optional integration with ImageMagick as a new image manipulation backend
* Support for PHP 5.4's built-in webserver
@ -26,6 +29,78 @@
## Upgrading
### Deny URL access if `Controller::$allowed_actions` is undefined or empty array
In order to make controller access checks more consistent and easier to
understand, the routing will require definition of `$allowed_actions`
on your own `Controller` subclasses if they contain any actions
accessible through URLs, or any forms.
:::php
class MyController extends Controller {
// This action is now denied because no $allowed_actions are specified
public function myaction($request) {}
}
Please review all rules governing allowed actions in the
["controller" topic](/topics/controller).
### Removed support for "*" rules in `Controller::$allowed_actions`
The wildcard ('*') character allowed to define fallback rules
in case they weren't explicitly defined. This caused a lot of confusion,
particularly around inherited rules. We've decided to remove the feature,
you'll need to specificy each accessible action individually.
:::php
class MyController extends Controller {
public static $allowed_actions = array('*' => 'ADMIN');
// Always denied because not explicitly listed in $allowed_actions
public function myaction($request) {}
// Always denied because not explicitly listed in $allowed_actions
public function myotheraction($request) {}
}
Please review all rules governing allowed actions in the
["controller" topic](/topics/controller).
### Removed support for overriding rules on parent classes through `Controller::$allowed_actions`
Since 3.1, the `$allowed_actions` definitions only apply
to methods defined on the class they're also defined on.
Overriding inherited access definitions is no longer possible.
:::php
class MyController extends Controller {
public static $allowed_actions = array('myaction' => 'ADMIN');
public function myaction($request) {}
}
class MySubController extends MyController {
// No longer works
public static $allowed_actions = array('myaction' => 'CMS_ACCESS_CMSMAIN');
}
This also applies for custom implementations of `handleAction()` and `handleRequest()`,
which now have to be listed in the `$allowed_actions` specifically.
It also restricts `Extension` classes applied to controllers, which now
can only grant or deny access or methods they define themselves.
New approach with the [Config API](/topics/configuration)
:::php
class MySubController extends MyController {
public function init() {
parent::init();
Config::inst()->update('MyController', 'allowed_actions',
array('myaction' => 'CMS_ACCESS_CMSMAIN')
);
}
}
Please review all rules governing allowed actions in the
["controller" topic](/topics/controller).
### Grouped CMS Buttons
The CMS buttons are now grouped, in order to hide minor actions by default and declutter the interface.

View File

@ -11,6 +11,8 @@ For information on how to upgrade to newer versions consult the [upgrading](/ins
* [3.1.0](3.1.0) - Unreleased
* [3.0.4](3.0.4) - 19 February 2013
* [3.0.3](3.0.3) - 26 November 2012
* [3.0.2](3.0.2) - 17 September 2012
* [3.0.1](3.0.1) - 31 July 2012
* [3.0.0](3.0.0) - 28 June 2012

View File

@ -68,6 +68,9 @@ You can have more customized logic and interface feedback through a custom contr
:::php
<?php
class MyController extends Controller {
static $allowed_actions = array('Form');
protected $template = "BlankPage";
public function Link($action = null) {

View File

@ -9,6 +9,7 @@ Let's start by defining a new `ContactPage` page type:
class ContactPage extends Page {
}
class ContactPage_Controller extends Page_Controller {
static $allowed_actions = array('Form');
public function Form() {
$fields = new FieldList(
new TextField('Name'),
@ -60,6 +61,7 @@ Now that we have a contact form, we need some way of collecting the data submitt
:::php
class ContactPage_Controller extends Page_Controller {
static $allowed_actions = array('Form');
public function Form() {
// ...
}

View File

@ -19,7 +19,7 @@ configuration settings:
index index.php index.html index.htm;
server_name example.com;
include silverstripe3;
include htaccess;
}
@ -29,7 +29,7 @@ Here is the include file `silverstripe3`:
location / {
try_files $uri @silverstripe;
}
location @silverstripe {
include fastcgi_params;
@ -68,6 +68,11 @@ Here is the include file `htaccess`:
try_files $uri $uri/ =404;
}
# Block access to yaml files
location ~ \.yml$ {
deny all;
}
# cms & framework .htaccess rules
location ~ ^/(cms|framework|mysite)/.*\.(php|php[345]|phtml|inc)$ {
deny all;

View File

@ -26,4 +26,18 @@ name' and the default login details. Follow the questions and select the *instal
## Issues?
If the above steps don't work for any reason have a read of the [Common Problems](common-problems) section.
If the above steps don't work for any reason have a read of the [Common Problems](common-problems) section.
## Security notes
### Yaml
For the reasons explained in [security](/topics/security) Yaml files are blocked by default by the .htaccess file
provided by the SilverStripe installer module.
To allow serving yaml files from a specific directory, add code like this to an .htaccess file in that directory
<Files *.yml>
Order allow,deny
Allow from all
</Files>

View File

@ -79,7 +79,7 @@ left-join for robustness; if there is no matching record in Page, we can return
SilverStripe has a powerful tool for automatically building database schemas. We've designed it so that you should never have to build them manually.
To access it, visit (site-root)/dev/build?flush=1. This script will analyze the existing schema, compare it to what's required by your data classes, and alter the schema as required.
To access it, visit http://<mysite>/dev/build?flush=1. This script will analyze the existing schema, compare it to what's required by your data classes, and alter the schema as required.
Put the ?flush=1 on the end if you've added PHP files, so that the rest of the system will find these new classes.

View File

@ -79,6 +79,7 @@ You can access the following controller-method with /team/signup
class Team extends DataObject {}
class Team_Controller extends Controller {
static $allowed_actions = array('signup');
public function signup($id, $otherId) {
return $this->renderWith('MyTemplate');
}

View File

@ -41,6 +41,8 @@ Here is an example where we display a basic gridfield with the default settings:
:::php
class GridController extends Page_Controller {
static $allowed_actions = array('index');
public function index(SS_HTTPRequest $request) {
$this->Content = $this->AllPages();

View File

@ -22,6 +22,57 @@ _Versioned_. They are not general enough for using on any other DataObject. Tha
of the feature.
</div>
## Configuration and Defaults
Like most of the CMS, the preview UI is powered by
[jQuery entwine](https://github.com/hafriedlander/jquery.entwine).
This means its defaults are configured through JavaScript, by setting entwine properties.
In order to achieve this, create a new file `mysite/javascript/MyLeftAndMain.Preview.js`.
In the following example we configure three aspects:
* Set the default mode from "split view" to a full "edit view"
* Make a wider mobile preview
* Increase minimum space required by preview before auto-hiding
Note how the configuration happens in different entwine namespaces
("ss.preview" and "ss"), as well as applies to different selectors
(".cms-preview" and ".cms-container").
:::js
(function($) {
$.entwine('ss.preview', function($){
$('.cms-preview').entwine({
DefaultMode: 'content',
getSizes: function() {
var sizes = this._super();
sizes.mobile.width = '400px';
return sizes;
}
});
});
$.entwine('ss', function($){
$('.cms-container').entwine({
getLayoutOptions: function() {
var opts = this._super();
opts.minPreviewWidth = 600;
return opts;
}
});
});
});
}(jQuery));
Load the file in the CMS via an addition to `mysite/_config.php`:
:::php
LeftAndMain::require_javascript('mysite/javascript/MyLeftAndMain.Preview.js');
In order to find out which configuration values are available, the source code
is your best reference at the moment - have a look in `framework/admin/javascript/LeftAndMain.Preview.js`.
To understand how layouts are handled in the CMS UI, have a look at the
[CMS Architecture](/reference/cms-architecture) guide.
## Enabling preview
The frontend decides on the preview being enabled or disabled based on the presnce of the `.cms-previewable` class. If

View File

@ -56,6 +56,16 @@ By default it stores the generated file in the assets/ folder but you can config
If SilverStripe doesn't have permissions on your server to write these files it will default back to including them
individually .
You can also combine CSS files into a media-specific stylesheets as you would with the `Requirements::css` call - use
the third paramter of the `combine_files` function:
:::php
$printStylesheets = array(
"$themeDir/css/print_HomePage.css",
"$themeDir/css/print_Page.css",
);
Requirements::combine_files('print.css', $printStylesheets, 'print');
## Custom Inline Scripts
You can also quote custom script directly. This may seem a bit ugly, but is useful when you need to transfer some kind
@ -183,4 +193,4 @@ slightly different JS/CSS requirements, the whole lot will be refetched.
nature of an ajax-request. Needs some more research
## API Documentation
`[api:Requirements]`
`[api:Requirements]`

View File

@ -0,0 +1,163 @@
# Shortcodes
The Shortcode API is a way to replace simple bbcode-like tags within HTML. It is inspired by and very similar to
the [Wordpress implementation](http://codex.wordpress.org/Shortcode_API) of shortcodes.
A guide to syntax
Unclosed - [shortcode]
Explicitly closed - [shortcode/]
With parameters, mixed quoting - [shortcode parameter=value parameter2='value2' parameter3="value3"]
Old style parameter separation - [shortcode,parameter=value,parameter2='value2',parameter3="value3"]
With contained content & closing tag - [shortcode]Enclosed Content[/shortcode]
Escaped (will output [just] [text] in response) - [[just] [[text]]
Shortcode parsing is already hooked into HTMLText and HTMLVarchar fields when rendered into a template
## Attribute and element scope
HTML with unprocessed shortcodes in it is still valid HTML. As a result, shortcodes can be in two places in HTML:
- In an attribute value, like so:
<a title="[title]">link</a>
- In an element's text, like so:
<p>
Some text [shortcode] more text
</p>
The first is called "element scope" use, the second "attribute scope"
You may not use shortcodes in any other location. Specifically, you can not use shortcodes to generate attributes or
change the name of a tag. These usages are forbidden:
<[paragraph]>Some test</[paragraph]>
<a [titleattribute]>link</a>
Also note:
- you may need to escape text inside attributes `>` becomes `&gt;` etc
- you can include HTML tags inside a shortcode tag, but you need to be careful of nesting to ensure you don't
break the output
Good:
<div>
[shortcode]
<p>Caption</p>
[/shortcode]
</div>
Bad:
<div>
[shortcode]
</div>
<p>
[/shortcode]
</p>
## Location
Element scoped shortcodes have a special ability to move the location they are inserted at to comply with
HTML lexical rules. Take for example this basic paragraph tag:
<p><a href="#">Head [figure src="assets/a.jpg" caption="caption"] Tail</a></p>
When converted naively would become
<p><a href="#">Head <figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure> Tail</a></p>
However this is not valid HTML - P elements can not contain other block level elements.
To fix this you can specify a "location" attribute on a shortcode. When the location attribute is "left" or "right"
the inserted content will be moved to immediately before the block tag. The result is this:
<figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure><p><a href="#">Head Tail</a></p>
When the location attribute is "leftAlone" or "center" then the DOM is split around the element. The result is this:
<p><a href="#">Head </a></p><figure><img src="assets/a.jpg" /><figcaption>caption</figcaption></figure><p><a href="#"> Tail</a></p>
## Defining Custom Shortcodes
All you need to do to define a shortcode is to register a callback with the parser that will be called whenever a
shortcode is encountered. This callback will return a string to replace the shortcode with.
:::php
public static function my_shortcode_handler($attributes, $enclosedContent, $parser, $tagName) {
// This simple callback simply converts the shortcode to a span.
return "<span class=\"$tagName\">$enclosedContent</span>";
}
The parameters passed to the callback are, in order:
* Any parameters attached to the shortcode as an associative array (keys are lower-case).
* Any content enclosed within the shortcode (if it is an enclosing shortcode). Note that any content within this will
not have been parsed, and can optionally be fed back into the parser.
* The ShortcodeParser instance used to parse the content.
* The shortcode tag name that was matched within the parsed content.
For the shortcode to work, you need to register it with the `ShortcodeParser`. Assuming you've placed the
callback function in the `Page` class, you would need to make the following call from `_config.php`:
:::php
ShortcodeParser::get('default')->register(
'shortcode_tag_name',
array('Page', 'my_shortcode_handler')
);
An example result of installing such a shortcode would be that the string `[shortcode_tag_name]Testing
testing[/shortcode_tag_name]` in the page *Content* would be replaced with the `<span class="shortcode_tag_name">Testing
testing</span>`.
### Parameter values
Here is a summary of the callback parameter values based on some example shortcodes.
#### Short
[my_shortcodes]
$attributes => array()
$enclosedContent => null
$parser => ShortcodeParser instance
$tagName => 'my_shortcode'
#### Short with attributes
[my_shortcode,attribute="foo",other="bar"]
$attributes => array ('attribute' => 'foo', 'other' => 'bar')
$enclosedContent => null
$parser => ShortcodeParser instance
$tagName => 'my_shortcode'
#### Long with attributes
[my_shortcode,attribute="foo"]content[/my_shortcode]
$attributes => array('attribute' => 'foo')
$enclosedContent => 'content'
$parser => ShortcodeParser instance
$tagName => 'my_shortcode'
## Inbuilt Shortcodes
All internal links inserted via the CMS into a content field are in the form `<a href="[sitetree_link,id=n]">`. At
runtime this is replaced by a plain link to the page with the ID in question.
## Limitations
Since the shortcode parser is based on a simple regular expression it cannot properly handle nested shortcodes. For
example the below code will not work as expected:
[shortcode]
[shortcode][/shortcode]
[/shortcode]
The parser will raise an error if it can not find a matching opening tag for any particular closing tag

View File

@ -37,8 +37,8 @@ We'll explain some ways to use *SELECT* with the full power of SQL,
but still maintain a connection to the ORM where possible.
<div class="warning" markdown="1">
Please read our ["security" topic](/topics/security) to find out
how to sanitize user input before using it in SQL queries.
Please read our ["security" topic](/topics/security) to find out
how to sanitize user input before using it in SQL queries.
</div>
## Usage
@ -140,4 +140,4 @@ An alternative approach would be a custom getter in the object definition.
* [datamodel](/topics/datamodel)
* `[api:DataObject]`
* [database-structure](database-structure)
* [database-structure](database-structure)

View File

@ -568,6 +568,8 @@ default if it exists and there is no action in the url parameters.
:::php
class MyPage_Controller extends Page_Controller {
static $allowed_actions = array('index');
public function init(){
parent::init();

View File

@ -26,8 +26,8 @@ Append the option and corresponding value to your URL in your browser's address
| URL Variable | | Values | | Description |
| ------------ | | ------ | | ----------- |
| isDev | | 1 | | Put the site into [development mode](/topics/debugging), enabling debugging messages to the browser on a live server. For security, you'll be asked to log in with an administrator log-in |
| isTest | | 1 | | Put the site into [test mode](/topics/debugging), enabling debugging messages to the admin email and generic errors to the browser on a live server |
| isDev | | 1 | | Put the site into [development mode](/topics/debugging), enabling debugging messages to the browser on a live server. For security, you'll be asked to log in with an administrator log-in. Will persist for the current browser session. |
| isTest | | 1 | | See above. |
| debug | | 1 | | Show a collection of debugging information about the director / controller operation |
| debug_request | | 1 | | Show all steps of the request from initial `[api:HTTPRequest]` to `[api:Controller]` to Template Rendering |

View File

@ -3,7 +3,7 @@
Base controller class. You will extend this to take granular control over the
actions and url handling of aspects of your SilverStripe site.
## Example
## Usage
The following example is for a simple `[api:Controller]` class. If you're using
the cms module and looking at Page_Controller instances you won't need to setup
@ -15,11 +15,14 @@ your own routes since the cms module handles these routes.
<?php
class FastFood_Controller extends Controller {
public function order($arguments) {
public static $allowed_actions = array('order');
public function order(SS_HTTPRequest $request) {
print_r($arguments);
}
}
## Routing
`mysite/_config/routes.yml`
:::yaml
@ -44,6 +47,100 @@ making any code changes to your controller.
[Name] => cheesefries
)
<div class="warning" markdown='1'>
SilverStripe automatically adds a URL routing entry based on the controller's class name,
so a `MyController` class is accessible through `http://yourdomain.com/MyController`.
</div>
## Access Control
### Through $allowed_actions
All public methods on a controller are accessible by their name through the `$Action`
part of the URL routing, so a `MyController->mymethod()` is accessible at
`http://yourdomain.com/MyController/mymethod`. This is not always desireable,
since methods can return internal information, or change state in a way
that's not intended to be used through a URL endpoint.
SilverStripe strongly recommends securing your controllers
through defining a `$allowed_actions` array on the class,
which allows whitelisting of methods, as well as a concise
way to perform checks against permission codes or custom logic.
:::php
class MyController extends Controller {
public static $allowed_actions = array(
// someaction can be accessed by anyone, any time
'someaction',
// So can otheraction
'otheraction' => true,
// restrictedaction can only be people with ADMIN privilege
'restrictedaction' => 'ADMIN',
// complexaction can only be accessed if $this->canComplexAction() returns true
'complexaction' '->canComplexAction'
);
}
There's a couple of rules guiding these checks:
* Each class is only responsible for access control on the methods it defines
* If `$allowed_actions` is defined as an empty array, no actions are allowed
* If `$allowed_actions` is undefined, all public methods on the specific class are allowed
(not recommended)
* Access checks on parent classes need to be overwritten via the Config API
* Only public methods can be made accessible
* If a method on a parent class is overwritten, access control for it has to be redefined as well
* An action named "index" is whitelisted by default,
unless allowed_actions is defined as an empty array,
or the action is specifically restricted in there.
* Methods returning forms also count as actions which need to be defined
* Form action methods (targets of `FormAction`) should NOT be included in `$allowed_actions`,
they're handled separately through the form routing (see the ["forms" topic](/topics/forms))
* `$allowed_actions` can be defined on `Extension` classes applying to the controller.
If the permission check fails, SilverStripe will return a "403 Forbidden" HTTP status.
### Through the action
Each method responding to a URL can also implement custom permission checks,
e.g. to handle responses conditionally on the passed request data.
:::php
class MyController extends Controller {
public static $allowed_actions = array('myaction');
public function myaction($request) {
if(!$request->getVar('apikey')) {
return $this->httpError(403, 'No API key provided');
}
return 'valid';
}
}
Unless you transform the response later in the request processing,
it'll look pretty ugly to the user. Alternatively, you can use
`ErrorPage::response_for(<status-code>)` to return a more specialized layout.
Note: This is recommended as an addition for `$allowed_actions`, in order to handle
more complex checks, rather than a replacement.
### Through the init() method
After checking for allowed_actions, each controller invokes its `init()` method,
which is typically used to set up common state in the controller, and
include JavaScript and CSS files in the output which are used for any action.
If an `init()` method returns a `SS_HTTPResponse` with either a 3xx or 4xx HTTP
status code, it'll abort execution. This behaviour can be used to implement
permission checks.
:::php
class MyController extends Controller {
public static $allowed_actions = array();
public function init() {
parent::init();
if(!Permission::check('ADMIN')) return $this->httpError(403);
}
}
## URL Handling
@ -60,11 +157,26 @@ through `/fastfood/drivethrough/` to use the same order function.
:::php
class FastFood_Controller extends Controller {
static $allowed_actions = array('drivethrough');
public static $url_handlers = array(
'drivethrough/$Action/$ID/$Name' => 'order'
);
## Access Control
### Through $allowed_actions
* If `$allowed_actions` is undefined, `null` or `array()`, no actions are accessible
* Each class is only responsible for access control on the methods it defines
* Access checks on parent classes need to be overwritten via the Config API
* Only public methods can be made accessible
* If a method on a parent class is overwritten, access control for it has to be redefined as well
* An action named "index" is whitelisted by default
* Methods returning forms also count as actions which need to be defined
* Form action methods (targets of `FormAction`) should NOT be included in `$allowed_actions`,
they're handled separately through the form routing (see the ["forms" topic](/topics/forms))
* `$allowed_actions` can be defined on `Extension` classes applying to the controller.
## URL Patterns
The `[api:RequestHandler]` class will parse all rules you specify against the

View File

@ -23,7 +23,8 @@ The SilverStripe database-schema is generated automatically by visiting the URL.
`http://<mysite>/dev/build`
<div class="notice" markdown='1'>
Note: You need to be logged in as an administrator to perform this command.
Note: You need to be logged in as an administrator to perform this command,
unless your site is in "[dev mode](/topics/debugging)", or the command is run through CLI.
</div>
## Querying Data

View File

@ -56,10 +56,8 @@ of `$databaseConfig` and `Director::set_dev_servers`, and instead make sure that
The mechanism by which the `_ss_environment.php` files work is quite simple. Here's how it works:
* At the beginning of SilverStripe's execution, the `_ss_environment.php` file is searched for, and if it is found, it's
included. SilverStripe looks in 3 places for the file:
* The site's base folder (ie, a sibling of framework, jsparty, and cms)
* The parent of the base folder
* The grandparent of the base folder
included. SilverStripe looks in all the parent folders of framework up to the server root (using the REAL location of
the dir - see PHP realpath()):
* The `_ss_environment.php` file sets a number of "define()".
* "conf/ConfigureFromEnv.php" is included from within your `mysite/_config.php`. This file has a number of regular
configuration commands that use those defines as their arguments. If you are curious, open up

View File

@ -49,6 +49,7 @@ Example: Validate postcodes based on the selected country (on the controller).
:::php
class MyController extends Controller {
static $allowed_actions = array('Form');
public function Form() {
return Form::create($this, 'Form',
new FieldList(

View File

@ -25,9 +25,7 @@ Forms start at the controller. Here is an simple example on how to set up a form
:::php
class Page_Controller extends ContentController {
public static $allowed_actions = array(
'HelloForm',
);
public static $allowed_actions = array('HelloForm');
// Template method
public function HelloForm() {
@ -41,11 +39,24 @@ Forms start at the controller. Here is an simple example on how to set up a form
return $form;
}
public function doSayHello(array $data, Form $form) {
public function doSayHello($data, Form $form) {
// Do something with $data
return $this->render();
}
}
The name of the form ("HelloForm") is passed into the `Form`
constructor as a second argument. It needs to match the method name.
Since forms need a URL, the `HelloForm()` method needs to be handled
like any other controller action. In order to whitelist its access through
URLs, we add it to the `$allowed_actions` array.
Form actions ("doSayHello") on the other hand should NOT be included here,
these are handled separately through `Form->httpSubmission()`.
You can control access on form actions either by conditionally removing
a `FormAction` from the form construction,
or by defining `$allowed_actions` in your own `Form` class
(more information in the ["controllers" topic](/topics/controllers)).
**Page.ss**

View File

@ -9,13 +9,13 @@ you can effectively select and upload files.
## Usage
The framework comes with a `[api:HTMLEditorField]` form field class which encapsulates most of the required functionality.
It is usually added through the `[api:DataObject->getCMSFields()]` method:
The framework comes with a `[api:HTMLEditorField]` form field class which encapsulates most of the required
functionality. It is usually added through the `[api:DataObject->getCMSFields()]` method:
:::php
class MyObject extends DataObject {
static $db = array('Content' => 'HTMLText');
public function getCMSFields() {
return new FieldList(new HTMLEditorField('Content'));
}
@ -27,25 +27,128 @@ To keep the JavaScript editor configuration manageable and extensible,
we've wrapped it in a PHP class called `[api:HtmlEditorConfig]`.
The class comes with its own defaults, which are extended through the `_config.php`
files in the framework (and the `cms` module in case you've got that installed).
There can be multiple configs, which should always be created / accessed using `[api:HtmlEditorConfig::get].
You can then set the currently active config using `set_active()`.
There can be multiple configs, which should always be created / accessed using `[api:HtmlEditorConfig::get]`.
You can then set the currently active config using `set_active()`.
By default, a config named 'cms' is used in any field created throughout the CMS interface.
Example: Enable the "media" plugin:
<div class="notice" markdown='1'>
Caveat: currently the order in which the `_config.php` files are executed depends on the module directory
names. Execution order is alphabetical, so if you set a TinyMCE option in the `aardvark/_config.php`, this
will be overriden in `framework/admin/_config.php` and your modification will disappear.
This is a general problem with `_config.php` files - it may be fixed in the future by making it possible to
configure the TinyMCE with the new [configuration system](../topics/configuration).
</div>
### Adding and removing capabilities
In its simplest form, the configuration of the editor includes adding and removing buttons and plugins.
You can add plugins to the editor using the Framework's `[api:HtmlEditorConfig::enablePlugins]` method. This will
transparently generate the relevant underlying TinyMCE code.
:::php
// File: mysite/_config.php
HtmlEditorConfig::get('cms')->enablePlugins('media');
Example: Remove some buttons for more advanced formatting
Note: this utilises the TinyMCE's `PluginManager::load` function under the hood (check the
[TinyMCE documentation on plugin
loading](http://www.tinymce.com/wiki.php/API3:method.tinymce.AddOnManager.load) for details).
Plugins and advanced themes can provide additional buttons that can be added (or removed) through the
configuration. Here is an example of adding a `ssmacron` button after the `charmap` button:
:::php
// File: mysite/_config.php
HtmlEditorConfig::get('cms')->insertButtonsAfter('charmap', 'ssmacron');
Buttons can also be removed:
:::php
// File: mysite/_config.php
HtmlEditorConfig::get('cms')->removeButtons('tablecontrols', 'blockquote', 'hr');
Note: internally `[api:HtmlEditorConfig]` uses the TinyMCE's `theme_advanced_buttons` option to configure these. See
the [TinyMCE documentation of this option](http://www.tinymce.com/wiki.php/Configuration:theme_advanced_buttons_1_n)
for more details.
### Setting options
TinyMCE behaviour can be affected through its [configuration options](http://www.tinymce.com/wiki.php/Configuration).
These options will be passed straight to the editor.
One example of the usage of this capability is to redefine the TinyMCE's [whitelist of HTML
tags](http://www.tinymce.com/wiki.php/Configuration:extended_valid_elements) - the tags that will not be stripped
from the HTML source by the editor.
:::php
// Add start and type attributes for <ol>, add <object> and <embed> with all attributes.
HtmlEditorConfig::get('cms')->setOption(
'extended_valid_elements',
'img[class|src|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name|usemap],' .
'iframe[src|name|width|height|title|align|allowfullscreen|frameborder|marginwidth|marginheight|scrolling],' .
'object[classid|codebase|width|height|data|type],' .
'embed[src|type|pluginspage|width|height|autoplay],' .
'param[name|value],' .
'map[class|name|id],' .
'area[shape|coords|href|target|alt],' .
'ol[start|type]'
);
Note: the default setting for the CMS's `extended_valid_elements` we are overriding here can be found in
`framework/admin/_config.php`.
### Writing custom plugins
It is also possible to add custom buttons to TinyMCE. A simple example of this is SilverStripe's `ssmacron`
plugin. The source can be found in the Framework's `thirdparty/tinymce_ssmacron` directory.
Here is how we can create a project-specific plugin. Create a `mysite/javascript/myplugin` directory,
add the plugin button icon - here `myplugin.png` - and the source code - here `editor_plugin.js`. Here is a very
simple example of a plugin that adds a button to the editor:
:::js
(function() {
tinymce.create('tinymce.plugins.myplugin', {
init : function(ed, url) {
var self = this;
ed.addButton ('myplugin', {
'title' : 'My plugin',
'image' : url+'/myplugin.png',
'onclick' : function () {
alert('Congratulations! Your plugin works!');
}
});
},
getInfo : function() {
return {
longname : 'myplugin',
author : 'Me',
authorurl : 'http://me.org.nz/',
infourl : 'http://me.org.nz/myplugin/',
version : "1.0"
};
}
});
tinymce.PluginManager.add('myplugin', tinymce.plugins.myplugin);
})();
You can then enable this plugin through the `[api:HtmlEditorConfig::enablePlugins]`:
:::php
HtmlEditorConfig::get('cms')->enablePlugins(array('myplugin' => '../../../mysite/javascript/myplugin/editor_plugin.js'));
For more complex examples see the [Creating a Plugin](http://www.tinymce.com/wiki.php/Creating_a_plugin) in TinyMCE
documentation, or browse through plugins that come with the Framework at `thirdparty/tinymce/plugins`.
## Image and Media Insertion
The `[api:HtmlEditorField]` API also handles inserting images and media
The `[api:HtmlEditorField]` API also handles inserting images and media
files into the managed HTML content. It can be used both for referencing
files on the webserver filesystem (through the `[api:File]` and `[api:Image]` APIs),
as well as hotlinking files from the web.
@ -60,7 +163,8 @@ its URL, as opposed to dealing with manual HTML code.
oEmbed powers the "Insert from web" feature available through `[api:HtmlEditorField]`.
Internally, it makes HTTP queries to a list of external services
if it finds a matching URL. These services are described in the `Oembed.providers` configuration.
Since these requests are performed on page rendering, they typically have a long cache time (multiple days). To refresh a cache, append `?flush=1` to a URL.
Since these requests are performed on page rendering, they typically have a long cache time (multiple days). To refresh
a cache, append `?flush=1` to a URL.
To disable oEmbed usage, set the `Oembed.enabled` configuration property to "false".

View File

@ -79,6 +79,7 @@ Example:
:::php
class MyController extends Controller {
static $allowed_actions = array('myurlaction');
public function myurlaction($RAW_urlParams) {
$SQL_urlParams = Convert::raw2sql($RAW_urlParams); // works recursively on an array
$objs = Player::get()->where("Name = '{$SQL_data[OtherID]}'");
@ -93,7 +94,6 @@ This means if you've got a chain of functions passing data through, escaping sho
:::php
class MyController extends Controller {
/**
* @param array $RAW_data All names in an indexed array (not SQL-safe)
*/
public function saveAllNames($RAW_data) {
@ -220,6 +220,7 @@ PHP:
:::php
class MyController extends Controller {
static $allowed_actions = array('search');
public function search($request) {
$htmlTitle = '<p>Your results for:' . Convert::raw2xml($request->getVar('Query')) . '</p>';
return $this->customise(array(
@ -249,6 +250,7 @@ PHP:
:::php
class MyController extends Controller {
static $allowed_actions = array('search');
public function search($request) {
$rssRelativeLink = "/rss?Query=" . urlencode($_REQUEST['query']) . "&sortOrder=asc";
$rssLink = Controller::join_links($this->Link(), $rssRelativeLink);
@ -363,6 +365,16 @@ file in the assets directory. This requires PHP to be loaded as an Apache modul
php_flag engine off
Options -ExecCGI -Includes -Indexes
### Don't allow access to .yml files
Yaml files are often used to store sensitive or semi-sensitive data for use by SilverStripe framework (for instance,
configuration and test fixtures).
You should therefore block access to all yaml files (extension .yml) by default, and white list only yaml files
you need to serve directly.
See [Apache](/installation/webserver) and [Nginx](/installation/nginx) installation documentation for details
specific to your web server
## Related

View File

@ -21,6 +21,8 @@ The poll we will be creating on our homepage will ask the user for their name an
:::php
class HomePage_Controller extends Page_Controller {
static $allowed_actions = array('BrowserPollForm');
// ...
public function BrowserPollForm() {

View File

@ -118,16 +118,54 @@ class Email extends ViewableData {
static $admin_email_address = '';
/**
* Send every email generated by the Email class to the given address.
*
* It will also add " [addressed to (email), cc to (email), bcc to (email)]" to the end of the subject line
*
* To set this, set Email.send_all_emails_to in your yml config file.
* It can also be set in _ss_environment.php with SS_SEND_ALL_EMAILS_TO.
*
* @param string $send_all_emails_to Email-Address
*/
protected static $send_all_emails_to = null;
/**
* Send every email generated by the Email class *from* the given address.
* It will also add " [, from to (email)]" to the end of the subject line
*
* To set this, set Email.send_all_emails_from in your yml config file.
* It can also be set in _ss_environment.php with SS_SEND_ALL_EMAILS_FROM.
*
* @param string $send_all_emails_from Email-Address
*/
protected static $send_all_emails_from = null;
/**
* BCC every email generated by the Email class to the given address.
* It won't affect the original delivery in the same way that send_all_emails_to does. It just adds a BCC header
* with the given email address. Note that you can only call this once - subsequent calls will overwrite the
* configuration variable.
*
* This can be used when you have a system that relies heavily on email and you want someone to be checking all
* correspondence.
*
* To set this, set Email.bcc_all_emails_to in your yml config file.
*
* @param string $bcc_all_emails_to Email-Address
*/
protected static $bcc_all_emails_to = null;
/**
* CC every email generated by the Email class to the given address.
* It won't affect the original delivery in the same way that send_all_emails_to does. It just adds a CC header
* with the given email address. Note that you can only call this once - subsequent calls will overwrite the
* configuration variable.
*
* This can be used when you have a system that relies heavily on email and you want someone to be checking all
* correspondence.
*
* To set this, set Email.cc_all_emails_to in your yml config file.
*
* @param string $cc_all_emails_to Email-Address
*/
protected static $cc_all_emails_to = null;
@ -388,37 +426,45 @@ class Email extends ViewableData {
if(project()) $headers['X-SilverStripeSite'] = project();
$to = $this->to;
$from = $this->from;
$subject = $this->subject;
if(self::$send_all_emails_to) {
if($sendAllTo = $this->config()->send_all_emails_to) {
$subject .= " [addressed to $to";
$to = self::$send_all_emails_to;
$to = $sendAllTo;
if($this->cc) $subject .= ", cc to $this->cc";
if($this->bcc) $subject .= ", bcc to $this->bcc";
$subject .= ']';
unset($headers['Cc']);
unset($headers['Bcc']);
} else {
if($this->cc) $headers['Cc'] = $this->cc;
if($this->bcc) $headers['Bcc'] = $this->bcc;
}
if(self::$cc_all_emails_to) {
if($ccAllTo = $this->config()->cc_all_emails_to) {
if(!empty($headers['Cc']) && trim($headers['Cc'])) {
$headers['Cc'] .= ', ' . self::$cc_all_emails_to;
$headers['Cc'] .= ', ' . $ccAllTo;
} else {
$headers['Cc'] = self::$cc_all_emails_to;
$headers['Cc'] = $ccAllTo;
}
}
if(self::$bcc_all_emails_to) {
if($bccAllTo = $this->config()->bcc_all_emails_to) {
if(!empty($headers['Bcc']) && trim($headers['Bcc'])) {
$headers['Bcc'] .= ', ' . self::$bcc_all_emails_to;
$headers['Bcc'] .= ', ' . $bccAllTo;
} else {
$headers['Bcc'] = self::$bcc_all_emails_to;
$headers['Bcc'] = $bccAllTo;
}
}
if($sendAllfrom = $this->config()->send_all_emails_from) {
if($from) $subject .= " [from $from]";
$from = $sendAllfrom;
}
Requirements::restore();
return self::mailer()->sendPlain($to, $this->from, $subject, $this->body, $this->attachments, $headers);
return self::mailer()->sendPlain($to, $from, $subject, $this->body, $this->attachments, $headers);
}
/**
@ -444,40 +490,49 @@ class Email extends ViewableData {
if(project()) $headers['X-SilverStripeSite'] = project();
$to = $this->to;
$from = $this->from;
$subject = $this->subject;
if(self::$send_all_emails_to) {
if($sendAllTo = $this->config()->send_all_emails_to) {
$subject .= " [addressed to $to";
$to = self::$send_all_emails_to;
$to = $sendAllTo;
if($this->cc) $subject .= ", cc to $this->cc";
if($this->bcc) $subject .= ", bcc to $this->bcc";
$subject .= ']';
unset($headers['Cc']);
unset($headers['Bcc']);
} else {
if($this->cc) $headers['Cc'] = $this->cc;
if($this->bcc) $headers['Bcc'] = $this->bcc;
}
if(self::$cc_all_emails_to) {
if($ccAllTo = $this->config()->cc_all_emails_to) {
if(!empty($headers['Cc']) && trim($headers['Cc'])) {
$headers['Cc'] .= ', ' . self::$cc_all_emails_to;
$headers['Cc'] .= ', ' . $ccAllTo;
} else {
$headers['Cc'] = self::$cc_all_emails_to;
$headers['Cc'] = $ccAllTo;
}
}
if(self::$bcc_all_emails_to) {
if($bccAllTo = $this->config()->bcc_all_emails_to) {
if(!empty($headers['Bcc']) && trim($headers['Bcc'])) {
$headers['Bcc'] .= ', ' . self::$bcc_all_emails_to;
$headers['Bcc'] .= ', ' . $bccAllTo;
} else {
$headers['Bcc'] = self::$bcc_all_emails_to;
$headers['Bcc'] = $bccAllTo;
}
}
if($sendAllfrom = $this->config()->send_all_emails_from) {
if($from) $subject .= " [from $from]";
$from = $sendAllfrom;
}
Requirements::restore();
return self::mailer()->sendHTML($to, $this->from, $subject, $this->body, $this->attachments, $headers,
return self::mailer()->sendHTML($to, $from, $subject, $this->body, $this->attachments, $headers,
$this->plaintext_body);
}
@ -511,7 +566,7 @@ class Email extends ViewableData {
public static function send_all_emails_to($emailAddress) {
self::$send_all_emails_to = $emailAddress;
}
/**
* CC every email generated by the Email class to the given address.
* It won't affect the original delivery in the same way that send_all_emails_to does. It just adds a CC header

View File

@ -27,7 +27,7 @@ class Mailer {
if ($customheaders && is_array($customheaders) == false) {
echo "htmlEmail($to, $from, $subject, ...) could not send mail: improper \$customheaders passed:<BR>";
dieprintr($customheaders);
}
}
// If the subject line contains extended characters, we must encode it
$subject = Convert::xml2raw($subject);
@ -53,9 +53,8 @@ class Mailer {
$messageParts[] = $this->encodeFileForEmail($file['tmp_name'], $file['name']);
} else {
$messageParts[] = $this->encodeFileForEmail($file);
}
}
}
}
// We further wrap all of this into another multipart block
list($fullBody, $headers) = $this->encodeMultipart($messageParts, "multipart/mixed");
@ -105,63 +104,60 @@ class Mailer {
}
/**
* Sends an email as a both HTML and plaintext
* Sends an email as a both HTML and plaintext
*
* $attachedFiles should be an array of file names
* - if you pass the entire $_FILES entry, the user-uploaded filename will be preserved
* use $plainContent to override default plain-content generation
*
* @return bool
*/
* $attachedFiles should be an array of file names
* - if you pass the entire $_FILES entry, the user-uploaded filename will be preserved
* use $plainContent to override default plain-content generation
*
* @return bool
*/
public function sendHTML($to, $from, $subject, $htmlContent, $attachedFiles = false, $customheaders = false,
$plainContent = false) {
if ($customheaders && is_array($customheaders) == false) {
echo "htmlEmail($to, $from, $subject, ...) could not send mail: improper \$customheaders passed:<BR>";
dieprintr($customheaders);
}
if ($customheaders && is_array($customheaders) == false) {
echo "htmlEmail($to, $from, $subject, ...) could not send mail: improper \$customheaders passed:<BR>";
dieprintr($customheaders);
}
$bodyIsUnicode = (strpos($htmlContent,"&#") !== false);
$plainEncoding = "";
// We generate plaintext content by default, but you can pass custom stuff
$plainEncoding = '';
if(!$plainContent) {
$plainContent = Convert::xml2raw($htmlContent);
if(isset($bodyIsUnicode) && $bodyIsUnicode) $plainEncoding = "base64";
}
$bodyIsUnicode = (strpos($htmlContent,"&#") !== false);
$plainEncoding = "";
// We generate plaintext content by default, but you can pass custom stuff
$plainEncoding = '';
if(!$plainContent) {
$plainContent = Convert::xml2raw($htmlContent);
if(isset($bodyIsUnicode) && $bodyIsUnicode) $plainEncoding = "base64";
}
// If the subject line contains extended characters, we must encode the
$subject = Convert::xml2raw($subject);
$subject = "=?UTF-8?B?" . base64_encode($subject) . "?=";
// If the subject line contains extended characters, we must encode the
$subject = Convert::xml2raw($subject);
$subject = "=?UTF-8?B?" . base64_encode($subject) . "?=";
// Make the plain text part
$headers["Content-Type"] = "text/plain; charset=utf-8";
$headers["Content-Transfer-Encoding"] = $plainEncoding ? $plainEncoding : "quoted-printable";
// Make the plain text part
$headers["Content-Type"] = "text/plain; charset=utf-8";
$headers["Content-Transfer-Encoding"] = $plainEncoding ? $plainEncoding : "quoted-printable";
$plainPart = $this->processHeaders($headers, ($plainEncoding == "base64")
? chunk_split(base64_encode($plainContent),60)
: wordwrap($this->QuotedPrintable_encode($plainContent),75));
// Make the HTML part
$headers["Content-Type"] = "text/html; charset=utf-8";
// Add basic wrapper tags if the body tag hasn't been given
if(stripos($htmlContent, '<body') === false) {
$htmlContent =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n" .
"<HTML><HEAD>\n" .
"<META http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n" .
"<STYLE type=\"text/css\"></STYLE>\n\n".
"</HEAD>\n" .
"<BODY bgColor=\"#ffffff\">\n" .
$htmlContent .
"\n</BODY>\n" .
"</HTML>";
}
// Make the HTML part
$headers["Content-Type"] = "text/html; charset=utf-8";
// Add basic wrapper tags if the body tag hasn't been given
if(stripos($htmlContent, '<body') === false) {
$htmlContent =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n" .
"<HTML><HEAD>\n" .
"<META http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n" .
"<STYLE type=\"text/css\"></STYLE>\n\n".
"</HEAD>\n" .
"<BODY bgColor=\"#ffffff\">\n" .
$htmlContent .
"\n</BODY>\n" .
"</HTML>";
}
$headers["Content-Transfer-Encoding"] = "quoted-printable";
$htmlPart = $this->processHeaders($headers, wordwrap($this->QuotedPrintable_encode($htmlContent),75));
@ -171,202 +167,205 @@ class Mailer {
"multipart/alternative"
);
// Messages with attachments are handled differently
if($attachedFiles && is_array($attachedFiles)) {
// The first part is the message itself
$fullMessage = $this->processHeaders($messageHeaders, $messageBody);
$messageParts = array($fullMessage);
// Include any specified attachments as additional parts
foreach($attachedFiles as $file) {
if(isset($file['tmp_name']) && isset($file['name'])) {
$messageParts[] = $this->encodeFileForEmail($file['tmp_name'], $file['name']);
} else {
$messageParts[] = $this->encodeFileForEmail($file);
}
}
// Messages with attachments are handled differently
if($attachedFiles && is_array($attachedFiles)) {
// We further wrap all of this into another multipart block
list($fullBody, $headers) = $this->encodeMultipart($messageParts, "multipart/mixed");
// The first part is the message itself
$fullMessage = $this->processHeaders($messageHeaders, $messageBody);
$messageParts = array($fullMessage);
// Messages without attachments do not require such treatment
} else {
$headers = $messageHeaders;
$fullBody = $messageBody;
}
// Include any specified attachments as additional parts
foreach($attachedFiles as $file) {
if(isset($file['tmp_name']) && isset($file['name'])) {
$messageParts[] = $this->encodeFileForEmail($file['tmp_name'], $file['name']);
} else {
$messageParts[] = $this->encodeFileForEmail($file);
}
}
// We further wrap all of this into another multipart block
list($fullBody, $headers) = $this->encodeMultipart($messageParts, "multipart/mixed");
// Email headers
// Messages without attachments do not require such treatment
} else {
$headers = $messageHeaders;
$fullBody = $messageBody;
}
// Email headers
$headers["From"] = $this->validEmailAddr($from);
// Messages with the X-SilverStripeMessageID header can be tracked
if(isset($customheaders["X-SilverStripeMessageID"]) && defined('BOUNCE_EMAIL')) {
$bounceAddress = BOUNCE_EMAIL;
} else {
$bounceAddress = $from;
}
// Messages with the X-SilverStripeMessageID header can be tracked
if(isset($customheaders["X-SilverStripeMessageID"]) && defined('BOUNCE_EMAIL')) {
$bounceAddress = BOUNCE_EMAIL;
} else {
$bounceAddress = $from;
}
// Strip the human name from the bounce address
if(preg_match('/^([^<>]*)<([^<>]+)> *$/', $bounceAddress, $parts)) $bounceAddress = $parts[2];
// Strip the human name from the bounce address
if(preg_match('/^([^<>]*)<([^<>]+)> *$/', $bounceAddress, $parts)) $bounceAddress = $parts[2];
// $headers["Sender"] = $from;
$headers["X-Mailer"] = X_MAILER;
if (!isset($customheaders["X-Priority"])) $headers["X-Priority"] = 3;
$headers = array_merge((array)$headers, (array)$customheaders);
// the carbon copy header has to be 'Cc', not 'CC' or 'cc' -- ensure this.
if (isset($headers['CC'])) { $headers['Cc'] = $headers['CC']; unset($headers['CC']); }
if (isset($headers['cc'])) { $headers['Cc'] = $headers['cc']; unset($headers['cc']); }
// the carbon copy header has to be 'Bcc', not 'BCC' or 'bcc' -- ensure this.
if (isset($headers['BCC'])) {$headers['Bcc']=$headers['BCC']; unset($headers['BCC']); }
if (isset($headers['bcc'])) {$headers['Bcc']=$headers['bcc']; unset($headers['bcc']); }
// $headers["Sender"] = $from;
$headers["X-Mailer"] = X_MAILER;
if (!isset($customheaders["X-Priority"])) $headers["X-Priority"] = 3;
// Send the email
$headers = array_merge((array)$headers, (array)$customheaders);
// the carbon copy header has to be 'Cc', not 'CC' or 'cc' -- ensure this.
if (isset($headers['CC'])) { $headers['Cc'] = $headers['CC']; unset($headers['CC']); }
if (isset($headers['cc'])) { $headers['Cc'] = $headers['cc']; unset($headers['cc']); }
// the carbon copy header has to be 'Bcc', not 'BCC' or 'bcc' -- ensure this.
if (isset($headers['BCC'])) {$headers['Bcc']=$headers['BCC']; unset($headers['BCC']); }
if (isset($headers['bcc'])) {$headers['Bcc']=$headers['bcc']; unset($headers['bcc']); }
// Send the email
$headers = $this->processHeaders($headers);
$to = $this->validEmailAddr($to);
// Try it without the -f option if it fails
if(!($result = @mail($to, $subject, $fullBody, $headers, escapeshellarg("-f$bounceAddress")))) {
$result = mail($to, $subject, $fullBody, $headers);
}
return $result;
}
/**
* @todo Make visibility protected in 3.2
*/
function encodeMultipart($parts, $contentType, $headers = false) {
$separator = "----=_NextPart_" . preg_replace('/[^0-9]/', '', rand() * 10000000000);
$headers["MIME-Version"] = "1.0";
$headers["Content-Type"] = "$contentType; boundary=\"$separator\"";
$headers["Content-Transfer-Encoding"] = "7bit";
if($contentType == "multipart/alternative") {
// $baseMessage = "This is an encoded HTML message. There are two parts: a plain text and an HTML message,
// open whatever suits you better.";
$baseMessage = "\nThis is a multi-part message in MIME format.";
} else {
// $baseMessage = "This is a message containing attachments. The e-mail body is contained in the first
// attachment";
$baseMessage = "\nThis is a multi-part message in MIME format.";
// Try it without the -f option if it fails
if(!($result = @mail($to, $subject, $fullBody, $headers, escapeshellarg("-f$bounceAddress")))) {
$result = mail($to, $subject, $fullBody, $headers);
}
return $result;
}
/**
* @todo Make visibility protected in 3.2
*/
function encodeMultipart($parts, $contentType, $headers = false) {
$separator = "----=_NextPart_" . preg_replace('/[^0-9]/', '', rand() * 10000000000);
$separator = "\n--$separator\n";
$body = "$baseMessage\n" .
$separator . implode("\n".$separator, $parts) . "\n" . trim($separator) . "--";
$headers["MIME-Version"] = "1.0";
$headers["Content-Type"] = "$contentType; boundary=\"$separator\"";
$headers["Content-Transfer-Encoding"] = "7bit";
return array($body, $headers);
}
if($contentType == "multipart/alternative") {
// $baseMessage = "This is an encoded HTML message. There are two parts: a plain text and an HTML message,
// open whatever suits you better.";
$baseMessage = "\nThis is a multi-part message in MIME format.";
} else {
// $baseMessage = "This is a message containing attachments. The e-mail body is contained in the first
// attachment";
$baseMessage = "\nThis is a multi-part message in MIME format.";
}
$separator = "\n--$separator\n";
$body = "$baseMessage\n" .
$separator . implode("\n".$separator, $parts) . "\n" . trim($separator) . "--";
return array($body, $headers);
}
/**
* @todo Make visibility protected in 3.2
*/
function processHeaders($headers, $body = false) {
$res = '';
if(is_array($headers)) {
while(list($k, $v) = each($headers)) {
$res .= "$k: $v\n";
}
}
if($body) $res .= "\n$body";
return $res;
}
/**
* @todo Make visibility protected in 3.2
*/
function processHeaders($headers, $body = false) {
$res = '';
if(is_array($headers)) while(list($k, $v) = each($headers))
$res .= "$k: $v\n";
if($body) $res .= "\n$body";
return $res;
}
/**
* Encode the contents of a file for emailing, including headers
*
* $file can be an array, in which case it expects these members:
* 'filename' - the filename of the file
* 'contents' - the raw binary contents of the file as a string
* and can optionally include these members:
* 'mimetype' - the mimetype of the file (calculated from filename if missing)
* 'contentLocation' - the 'Content-Location' header value for the file
*
* $file can also be a string, in which case it is assumed to be the filename
*
* h5. contentLocation
*
* Encode the contents of a file for emailing, including headers
*
* $file can be an array, in which case it expects these members:
* 'filename' - the filename of the file
* 'contents' - the raw binary contents of the file as a string
* and can optionally include these members:
* 'mimetype' - the mimetype of the file (calculated from filename if missing)
* 'contentLocation' - the 'Content-Location' header value for the file
*
* $file can also be a string, in which case it is assumed to be the filename
*
* h5. contentLocation
*
* Content Location is one of the two methods allowed for embedding images into an html email.
* It's also the simplest, and best supported.
*
* Assume we have an email with this in the body:
*
* <img src="http://example.com/image.gif" />
*
*
* Assume we have an email with this in the body:
*
* <img src="http://example.com/image.gif" />
*
* To display the image, an email viewer would have to download the image from the web every time
* it is displayed. Due to privacy issues, most viewers will not display any images unless
* the user clicks 'Show images in this email'. Not optimal.
*
*
* However, we can also include a copy of this image as an attached file in the email.
* By giving it a contentLocation of "http://example.com/image.gif" most email viewers
* will use this attached copy instead of downloading it. Better,
* most viewers will show it without a 'Show images in this email' conformation.
*
* Here is an example of passing this information through Email.php:
*
* $email = new Email();
* $email->attachments[] = array(
* 'filename' => BASE_PATH . "/themes/mytheme/images/header.gif",
* 'contents' => file_get_contents(BASE_PATH . "/themes/mytheme/images/header.gif"),
* 'mimetype' => 'image/gif',
* 'contentLocation' => Director::absoluteBaseURL() . "/themes/mytheme/images/header.gif"
* );
*
* most viewers will show it without a 'Show images in this email' conformation.
*
* Here is an example of passing this information through Email.php:
*
* $email = new Email();
* $email->attachments[] = array(
* 'filename' => BASE_PATH . "/themes/mytheme/images/header.gif",
* 'contents' => file_get_contents(BASE_PATH . "/themes/mytheme/images/header.gif"),
* 'mimetype' => 'image/gif',
* 'contentLocation' => Director::absoluteBaseURL() . "/themes/mytheme/images/header.gif"
* );
*
* @todo Make visibility protected in 3.2
*/
function encodeFileForEmail($file, $destFileName = false, $disposition = NULL, $extraHeaders = "") {
if(!$file) {
user_error("encodeFileForEmail: not passed a filename and/or data", E_USER_WARNING);
return;
}
if (is_string($file)) {
$file = array('filename' => $file);
$fh = fopen($file['filename'], "rb");
if ($fh) {
while(!feof($fh)) $file['contents'] .= fread($fh, 10000);
fclose($fh);
*/
function encodeFileForEmail($file, $destFileName = false, $disposition = NULL, $extraHeaders = "") {
if(!$file) {
user_error("encodeFileForEmail: not passed a filename and/or data", E_USER_WARNING);
return;
}
if (is_string($file)) {
$file = array('filename' => $file);
$fh = fopen($file['filename'], "rb");
if ($fh) {
$file['contents'] = "";
while(!feof($fh)) $file['contents'] .= fread($fh, 10000);
fclose($fh);
}
}
// Build headers, including content type
if(!$destFileName) $base = basename($file['filename']);
else $base = $destFileName;
$mimeType = !empty($file['mimetype']) ? $file['mimetype'] : HTTP::get_mime_type($file['filename']);
if(!$mimeType) $mimeType = "application/unknown";
if (empty($disposition)) $disposition = isset($file['contentLocation']) ? 'inline' : 'attachment';
// Encode for emailing
if (substr($mimeType, 0, 4) != 'text') {
$encoding = "base64";
$file['contents'] = chunk_split(base64_encode($file['contents']));
} else {
// This mime type is needed, otherwise some clients will show it as an inline attachment
$mimeType = 'application/octet-stream';
$encoding = "quoted-printable";
$file['contents'] = $this->QuotedPrintable_encode($file['contents']);
}
$headers = "Content-type: $mimeType;\n\tname=\"$base\"\n".
"Content-Transfer-Encoding: $encoding\n".
"Content-Disposition: $disposition;\n\tfilename=\"$base\"\n";
if ( isset($file['contentLocation']) ) $headers .= 'Content-Location: ' . $file['contentLocation'] . "\n" ;
$headers .= $extraHeaders . "\n";
// Return completed packet
return $headers . $file['contents'];
}
// Build headers, including content type
if(!$destFileName) $base = basename($file['filename']);
else $base = $destFileName;
$mimeType = $file['mimetype'] ? $file['mimetype'] : HTTP::get_mime_type($file['filename']);
if(!$mimeType) $mimeType = "application/unknown";
if (empty($disposition)) $disposition = isset($file['contentLocation']) ? 'inline' : 'attachment';
// Encode for emailing
if (substr($file['mimetype'], 0, 4) != 'text') {
$encoding = "base64";
$file['contents'] = chunk_split(base64_encode($file['contents']));
} else {
// This mime type is needed, otherwise some clients will show it as an inline attachment
$mimeType = 'application/octet-stream';
$encoding = "quoted-printable";
$file['contents'] = $this->QuotedPrintable_encode($file['contents']);
}
$headers = "Content-type: $mimeType;\n\tname=\"$base\"\n".
"Content-Transfer-Encoding: $encoding\n".
"Content-Disposition: $disposition;\n\tfilename=\"$base\"\n";
if ( isset($file['contentLocation']) ) $headers .= 'Content-Location: ' . $file['contentLocation'] . "\n" ;
$headers .= $extraHeaders . "\n";
// Return completed packet
return $headers . $file['contents'];
}
/**
* @todo Make visibility protected in 3.2
*/
function QuotedPrintable_encode($quotprint) {
function QuotedPrintable_encode($quotprint) {
$quotprint = (string)str_replace('\r\n',chr(13).chr(10),$quotprint);
$quotprint = (string)str_replace('\n', chr(13).chr(10),$quotprint);
$quotprint = (string)preg_replace_callback("~([\x01-\x1F\x3D\x7F-\xFF])~", function($matches) {
@ -378,25 +377,63 @@ function QuotedPrintable_encode($quotprint) {
$quotprint = (string)str_replace('=0D',"\n",$quotprint);
$quotprint = (string)str_replace('=0A',"\n",$quotprint);
return (string) $quotprint;
}
}
/**
* @todo Make visibility protected in 3.2
*/
function validEmailAddr($emailAddress) {
$emailAddress = trim($emailAddress);
$angBrack = strpos($emailAddress, '<');
if($angBrack === 0) {
$emailAddress = substr($emailAddress, 1, strpos($emailAddress,'>')-1);
function validEmailAddr($emailAddress) {
$emailAddress = trim($emailAddress);
$angBrack = strpos($emailAddress, '<');
if($angBrack === 0) {
$emailAddress = substr($emailAddress, 1, strpos($emailAddress,'>')-1);
} else if($angBrack) {
$emailAddress = str_replace('@', '', substr($emailAddress, 0, $angBrack))
. substr($emailAddress, $angBrack);
}
return $emailAddress;
}
/*
* Return a multipart/related e-mail chunk for the given HTML message and its linked images
* Decodes absolute URLs, accessing the appropriate local images
*/
function wrapImagesInline($htmlContent) {
global $_INLINED_IMAGES;
$_INLINED_IMAGES = null;
$replacedContent = imageRewriter($htmlContent, 'wrapImagesInline_rewriter($URL)');
// Make the HTML part
$headers["Content-Type"] = "text/html; charset=utf-8";
$headers["Content-Transfer-Encoding"] = "quoted-printable";
$multiparts[] = processHeaders($headers, QuotedPrintable_encode($replacedContent));
// Make all the image parts
global $_INLINED_IMAGES;
foreach($_INLINED_IMAGES as $url => $cid) {
$multiparts[] = encodeFileForEmail($url, false, "inline", "Content-ID: <$cid>\n");
}
// Merge together in a multipart
list($body, $headers) = encodeMultipart($multiparts, "multipart/related");
return processHeaders($headers, $body);
}
function wrapImagesInline_rewriter($url) {
$url = relativiseURL($url);
global $_INLINED_IMAGES;
if(!$_INLINED_IMAGES[$url]) {
$identifier = "automatedmessage." . rand(1000,1000000000) . "@silverstripe.com";
$_INLINED_IMAGES[$url] = $identifier;
}
return "cid:" . $_INLINED_IMAGES[$url];
} else if($angBrack) {
$emailAddress = str_replace('@', '', substr($emailAddress, 0, $angBrack))
.substr($emailAddress, $angBrack);
}
return $emailAddress;
}
}
/**

View File

@ -928,7 +928,7 @@ class File extends DataObject {
if(!is_array($exts)) $exts = array($exts);
foreach($exts as $ext) {
if(is_subclass_of($ext, 'File')) {
if(!is_subclass_of($ext, 'File')) {
throw new InvalidArgumentException(
sprintf('Class "%s" (for extension "%s") is not a valid subclass of File', $class, $ext)
);

View File

@ -58,8 +58,10 @@ class DatetimeField extends FormField {
public function __construct($name, $title = null, $value = ""){
$this->config = self::$default_config;
$this->dateField = DateField::create($name . '[date]', false);
$this->timeField = TimeField::create($name . '[time]', false);
$this->dateField = DateField::create($name . '[date]', false)
->addExtraClass('fieldgroup-field');
$this->timeField = TimeField::create($name . '[time]', false)
->addExtraClass('fieldgroup-field');
$this->timezoneField = new HiddenField($this->getName() . '[timezone]');
parent::__construct($name, $title, $value);
@ -80,6 +82,7 @@ class DatetimeField extends FormField {
'datetimeorder' => $this->getConfig('datetimeorder'),
);
$config = array_filter($config);
$this->addExtraClass('fieldgroup');
$this->addExtraClass(Convert::raw2json($config));
return parent::FieldHolder($properties);
@ -212,6 +215,24 @@ class DatetimeField extends FormField {
public function getDateField() {
return $this->dateField;
}
/**
* @param FormField
*/
public function setDateField($field) {
$expected = $this->getName() . '[date]';
if($field->getName() != $expected) {
throw new InvalidArgumentException(sprintf(
'Wrong name format for date field: "%s" (expected "%s")',
$field->getName(),
$expected
));
}
$field->setForm($this->getForm());
$this->dateField = $field;
$this->setValue($this->value); // update value
}
/**
* @return TimeField
@ -219,6 +240,24 @@ class DatetimeField extends FormField {
public function getTimeField() {
return $this->timeField;
}
/**
* @param FormField
*/
public function setTimeField($field) {
$expected = $this->getName() . '[time]';
if($field->getName() != $expected) {
throw new InvalidArgumentException(sprintf(
'Wrong name format for time field: "%s" (expected "%s")',
$field->getName(),
$expected
));
}
$field->setForm($this->getForm());
$this->timeField = $field;
$this->setValue($this->value); // update value
}
/**
* @return FormField
@ -236,15 +275,6 @@ class DatetimeField extends FormField {
public function getLocale() {
return $this->dateField->getLocale();
}
public function setDescription($description) {
parent::setDescription($description);
$this->dateField->setDescription($description);
$this->timeField->setDescription($description);
return $this;
}
/**
* Note: Use {@link getDateField()} and {@link getTimeField()}

View File

@ -250,7 +250,9 @@ class Form extends RequestHandler {
// Protection against CSRF attacks
$token = $this->getSecurityToken();
if(!$token->checkRequest($request)) {
$this->httpError(400, "Sorry, your session has timed out.");
$this->httpError(400, _t("Form.CSRF_FAILED_MESSAGE",
"There seems to have been a technical problem. Please click the back button,"
. " refresh your browser, and try again."));
}
// Determine the action button clicked
@ -655,7 +657,9 @@ class Form extends RequestHandler {
$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 (!($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);

View File

@ -37,21 +37,27 @@ class MemberDatetimeOptionsetField extends OptionsetField {
$value = ($this->value && !array_key_exists($this->value, $this->source)) ? $this->value : null;
$checked = ($value) ? " checked=\"checked\"" : '';
$options .= "<li class=\"valCustom\">"
. sprintf("<input id=\"%s_custom\" name=\"%s\" type=\"radio\" value=\"__custom__\" class=\"radio\" %s />",
$itemID, $this->name, $checked)
. sprintf('<label for="%s_custom">%s:</label>',
$itemID, _t('MemberDatetimeOptionsetField.Custom', 'Custom'))
. sprintf(
"<input id=\"%s_custom\" name=\"%s\" type=\"radio\" value=\"__custom__\" class=\"radio\" %s />",
$itemID, $this->name,
$checked
)
. sprintf(
'<label for="%s_custom">%s:</label>',
$itemID, _t('MemberDatetimeOptionsetField.Custom', 'Custom')
)
. sprintf(
"<input class=\"customFormat cms-help cms-help-tooltip\" name=\"%s_custom\" value=\"%s\" />\n",
$this->name,
$value
)
. sprintf("<input type=\"hidden\" class=\"formatValidationURL\" value=\"%s\" />",
$this->Link() . '/validate');
$this->name, Convert::raw2xml($value)
)
. sprintf(
"<input type=\"hidden\" class=\"formatValidationURL\" value=\"%s\" />",
$this->Link() . '/validate'
);
$options .= ($value) ? sprintf(
'<span class="preview">(%s: "%s")</span>',
_t('MemberDatetimeOptionsetField.Preview', 'Preview'),
Zend_Date::now()->toString($value)
Convert::raw2xml(Zend_Date::now()->toString($value))
) : '';
$id = $this->id();

View File

@ -35,7 +35,7 @@ class RequiredFields extends Validator {
* Clears all the validation from this object.
*/
public function removeValidation(){
$this->required = null;
$this->required = array();
}
/**

View File

@ -27,7 +27,7 @@ class TimeField extends TextField {
* @var array
*/
static $default_config = array(
'timeformat' => 'HH:mm:ss',
'timeformat' => null,
'use_strtotime' => true,
'datavalueformat' => 'HH:mm:ss'
);

View File

@ -421,6 +421,7 @@ class TreeDropdownField_Readonly extends TreeDropdownField {
$field = new LookupField($this->name, $this->title, $source);
$field->setValue($this->value);
$field->setForm($this->form);
$field->dontEscape = true;
return $field->Field();
}
}

View File

@ -612,7 +612,7 @@ class GridField extends FormField {
$stateChange = Session::get($id);
$actionName = $stateChange['actionName'];
$args = isset($stateChange['args']) ? $stateChange['args'] : array();
$html = $this->handleAction($actionName, $args, $data);
$html = $this->handleAlterAction($actionName, $args, $data);
// A field can optionally return its own HTML
if($html) return $html;
}
@ -642,7 +642,7 @@ class GridField extends FormField {
* @return type
* @throws InvalidArgumentException
*/
public function handleAction($actionName, $args, $data) {
public function handleAlterAction($actionName, $args, $data) {
$actionName = strtolower($actionName);
foreach($this->getComponents() as $component) {
if(!($component instanceof GridField_ActionProvider)) {

View File

@ -369,11 +369,11 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
$form->loadDataFrom($this->record, $this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT);
if($this->record->ID && !$canEdit) {
// Restrict editing of existing records
$form->makeReadonly();
// Restrict editing of existing records
$form->makeReadonly();
} elseif(!$this->record->ID && !$canCreate) {
// Restrict creation of new records
$form->makeReadonly();
// Restrict creation of new records
$form->makeReadonly();
}
// Load many_many extraData for record.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -135,7 +135,7 @@
}
});
$('.ss-gridfield .action.gridfield-button-delete').entwine({
$('.ss-gridfield .action.gridfield-button-delete, .cms-content-actions .ss-ui-action-destructive .ui-button-text').entwine({
onclick: function(e){
if(!confirm(ss.i18n._t('TABLEFIELD.DELETECONFIRMMESSAGE'))) {
e.preventDefault();

View File

@ -290,6 +290,16 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
this._super();
},
/**
* Make sure the editor has flushed all it's buffers before the form is submitted.
*/
'from .cms-edit-form': {
onbeforesubmitform: function(e) {
this.getEditor().save();
this._super();
}
},
oneditorinit: function() {
// Delayed show because TinyMCE calls hide() via setTimeout on removing an element,
// which is called in quick succession with adding a new editor after ajax loading new markup
@ -790,8 +800,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
// TODO Depends on managed mime type
if(node.is('img')) {
this.showFileView(node.data('url') || node.attr('src')).complete(function() {
$(this).updateFromNode(node);
this.showFileView(node.data('url') || node.attr('src')).done(function(filefield) {
filefield.updateFromNode(node);
self.toggleCloseButton();
self.redraw();
});
@ -834,25 +844,29 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
getFileView: function(idOrUrl) {
return this.find('.ss-htmleditorfield-file[data-id=' + idOrUrl + ']');
},
showFileView: function(idOrUrl, successCallback) {
var self = this, params = (Number(idOrUrl) == idOrUrl) ? {ID: idOrUrl} : {FileURL: idOrUrl},
item = $('<div class="ss-htmleditorfield-file" />');
showFileView: function(idOrUrl) {
var self = this, params = (Number(idOrUrl) == idOrUrl) ? {ID: idOrUrl} : {FileURL: idOrUrl};
item.addClass('loading');
var item = $('<div class="ss-htmleditorfield-file loading" />');
this.find('.content-edit').append(item);
return $.ajax({
// url: this.data('urlViewfile') + '?ID=' + id,
var dfr = $.Deferred();
$.ajax({
url: $.path.addSearchParams(this.attr('action').replace(/MediaForm/, 'viewfile'), params),
success: function(html, status, xhr) {
var newItem = $(html);
var newItem = $(html).filter('.ss-htmleditorfield-file');
item.replaceWith(newItem);
self.redraw();
if(successCallback) successCallback.call(newItem, html, status, xhr);
dfr.resolve(newItem);
},
error: function() {
item.remove();
dfr.reject();
}
});
return dfr.promise();
}
});
@ -941,7 +955,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
if (urlField.validate()) {
container.addClass('loading');
form.showFileView('http://' + urlField.val()).complete(function() {
form.showFileView('http://' + urlField.val()).done(function() {
container.removeClass('loading');
});
form.redraw();
@ -1043,44 +1057,69 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
};
},
getHTML: function() {
var el,
attrs = this.getAttributes(),
extraData = this.getExtraData(),
// imgEl = $('<img id="_ss_tmp_img" />');
imgEl = $('<img />').attr(attrs);
if(extraData.CaptionText) {
el = $('<div style="width: ' + attrs['width'] + 'px;" class="captionImage ' + attrs['class'] + '"><p class="caption">' + extraData.CaptionText + '</p></div>').prepend(imgEl);
} else {
el = imgEl;
}
return $('<div />').append(el).html(); // Little hack to get outerHTML string
/* NOP */
},
/**
* Logic similar to TinyMCE 'advimage' plugin, insertAndClose() method.
*/
insertHTML: function(ed) {
var form = this.closest('form'),
node = form.getSelection(), captionNode = node.closest('.captionImage');
var form = this.closest('form'), node = form.getSelection(), ed = form.getEditor();
if(node && node.is('img')) {
// If the image exists, update it to avoid complications with inserting TinyMCE HTML content
var attrs = this.getAttributes(), extraData = this.getExtraData();
node.attr(attrs);
// TODO Doesn't allow adding a caption to image after it was first added
if(captionNode.length) {
captionNode.find('.caption').text(extraData.CaptionText);
captionNode.css({width: attrs.width, height: attrs.height}).attr('class', attrs['class']);
// Get the attributes & extra data
var attrs = this.getAttributes(), extraData = this.getExtraData();
// Find the element we are replacing - either the img, it's wrapper parent, or nothing (if creating)
var replacee = (node && node.is('img')) ? node : null;
if (replacee && replacee.parent().is('.captionImage')) replacee = replacee.parent();
// Find the img node - either the existing img or a new one, and update it
var img = (node && node.is('img')) ? node : $('<img />');
img.attr(attrs);
// Any existing figure or caption node
var container = img.parent('.captionImage'), caption = container.find('.caption');
// If we've got caption text, we need a wrapping div.captionImage and sibling p.caption
if (extraData.CaptionText) {
if (!container.length) {
container = $('<div></div>');
}
// Undo needs to be added manually as we're doing direct DOM changes
ed.addUndo();
} else {
// Otherwise insert the whole HTML content
ed.repaint();
ed.insertContent(this.getHTML(), {skip_undo : 1});
ed.addUndo(); // Not sure why undo is separate here, replicating TinyMCE logic
container.attr('class', 'captionImage '+attrs['class']).css('width', attrs.width);
if (!caption.length) {
caption = $('<p class="caption"></p>').appendTo(container);
}
caption.attr('class', 'caption '+attrs['class']).text(extraData.CaptionText);
}
// Otherwise forget they exist
else {
container = caption = null;
}
// The element we are replacing the replacee with
var replacer = container ? container : img;
// If we're replacing something, and it's not with itself, do so
if (replacee && replacee.not(replacer).length) {
replacee.replaceWith(replacer);
}
// If we have a wrapper element, make sure the img is the first child - img might be the
// replacee, and the wrapper the replacer, and we can't do this till after the replace has happened
if (container) {
container.prepend(img);
}
// If we don't have a replacee, then we need to insert the whole HTML
if (!replacee) {
// Otherwise insert the whole HTML content
ed.repaint();
ed.insertContent($('<div />').append(replacer).html(), {skip_undo : 1});
}
ed.addUndo();
ed.repaint();
},
updateFromNode: function(node) {

View File

@ -159,7 +159,7 @@
));
if (this.data('fileupload')._isXHRUpload({multipart: true})) {
$('.ss-uploadfield-item-uploador').show();
$('.ss-uploadfield-item-uploador').hide().show();
dropZone.hide().show();
}

View File

@ -11,6 +11,31 @@ if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
'UNIQUEFIELD.CANNOTLEAVEEMPTY': 'Dit veld mag niet leeg blijven',
'RESTRICTEDTEXTFIELD.CHARCANTBEUSED': "Het karakter '%s' mag niet gebruikt worden in dit veld",
'UPDATEURL.CONFIRM': 'Wilt u de URL wijzigen naar:\n\n%s/\n\nKlik Ok om de URL te wijzigen, Klik Cancel om het'
+ ' te laten zoals het is:\n\n%s'
+ ' te laten zoals het is:\n\n%s',
'UPDATEURL.CONFIRMURLCHANGED':'Het URL is veranderd naar \n"%s"',
'FILEIFRAMEFIELD.DELETEFILE': 'Verwijder bestand',
'FILEIFRAMEFIELD.UNATTACHFILE': 'Deselecteer bestand',
'FILEIFRAMEFIELD.DELETEIMAGE': 'Verwijder afbeelding',
'FILEIFRAMEFIELD.CONFIRMDELETE': 'Weet u zeker dat u dit bestand wilt verwijderen?',
'LeftAndMain.IncompatBrowserWarning': 'Je huidige browser is niet compatible, gebruik één van deze browsers Internet Explorer 7+, Google Chrome 10+ or Mozilla Firefox 3.5+.',
'GRIDFIELD.ERRORINTRANSACTION': 'Er is een fout opgetreden bij het ophalen van gegevens van de server\n Probeer later opnieuw.',
'HtmlEditorField.SelectAnchor': 'Kies een anker',
'UploadField.ConfirmDelete': 'Weet u zeker dat u dit bestand wilt verwijderen uit het websitebestand?',
'UploadField.PHP_MAXFILESIZE': 'Bestandsgrootte is hoger dan upload_max_filesize (php.ini directive)',
'UploadField.HTML_MAXFILESIZE': 'Bestandsgrootte is hoger danMAX_FILE_SIZE (HTML form directive)',
'UploadField.ONLYPARTIALUPLOADED': 'Bestand is maar gedeeltelijk geupload',
'UploadField.NOFILEUPLOADED': 'Geen bestand is geupload',
'UploadField.NOTMPFOLDER': 'Mist een tijdelijke map',
'UploadField.WRITEFAILED': 'Kan bestand niet naar schijf schrijven',
'UploadField.STOPEDBYEXTENSION': 'Bestandsupload gestopt door extensie',
'UploadField.TOOLARGE': 'Bestandsgrootte is te groot',
'UploadField.TOOSMALL': 'Bestandsgrootte is te klein',
'UploadField.INVALIDEXTENSION': 'Extensie is niet toegestaan',
'UploadField.MAXNUMBEROFFILESSIMPLE': 'Maximaal aantal overschreven',
'UploadField.UPLOADEDBYTES': 'Upload overschrijd bestandsgrootte',
'UploadField.EMPTYRESULT': 'Leeg bestand geupload',
'UploadField.LOADING': 'Laden ...',
'UploadField.Editing': 'Bijwerken ...',
'UploadField.Uploaded': 'Geupload'
});
}

View File

@ -19,7 +19,7 @@ nl:
DROPAREA: 'Hierheen slepen'
EDITALL: 'Alle bewerken'
EDITANDORGANIZE: 'Bewerk en beheer'
EDITINFO: 'Edit files'
EDITINFO: 'Bewerk alle bestanden'
FILES: Bestanden
FROMCOMPUTER: 'Selecteer bestand op computer'
FROMCOMPUTERINFO: 'Upload from your computer'
@ -175,16 +175,16 @@ nl:
XlsType: 'Excel document'
ZipType: 'ZIP bestand'
FileIFrameField:
ATTACH: 'Attach {type}'
ATTACHONCESAVED: '{type}s can be attached once you have saved the record for the first time.'
ATTACHONCESAVED2: 'Files can be attached once you have saved the record for the first time.'
DELETE: 'Delete {type}'
ATTACH: 'Toevoegen {type}'
ATTACHONCESAVED: '{type}en kunnen worden toegevoegd na het voor het eerst opslaan.'
ATTACHONCESAVED2: 'Bestanden kunnen worden toegevoegd na het voor het eerst opslaan.'
DELETE: 'Verwijderen {type}'
DISALLOWEDFILETYPE: 'Dit type bestand mag niet worden opgeslagen'
FILE: Bestand
FROMCOMPUTER: 'Vanaf computer'
FROMFILESTORE: 'Vanaf de website''s bestandsopslag'
NOSOURCE: 'Selecteer een bron bestand om toe te voegen'
REPLACE: 'Replace {type}'
REPLACE: 'Vervang {type}'
FileIFrameField_iframe.ss:
TITLE: 'Afbeelding uploaden'
Filesystem:
@ -232,7 +232,7 @@ nl:
DeletePermissionsFailure: 'Onvoldoende rechten om te verwijderen'
GridFieldDetailForm:
CancelBtn: Annuleren
Create: Create
Create: Creëren
Delete: Verwijder
DeletePermissionsFailure: 'No delete permissions'
Deleted: '%s %s verwijderd'
@ -334,7 +334,7 @@ nl:
VersionUnknown: onbekend
LeftAndMain_Menu.ss:
Hello: Hi
LOGOUT: 'Log out'
LOGOUT: Uitloggen
LoginAttempt:
Email: 'Email adres '
IP: 'IP Adres'
@ -410,7 +410,7 @@ nl:
MemberImportForm:
Help1: '<p>Importeer leden in <em>CSV</em> formaat (Kommagescheiden bestandsformaat). <small><a href="#" class="toggle-advanced">Toon geavanceerd gebruik</a></small></p>'
Help2: '<div class="advanced"> <h4>Advanced usage</h4> <ul> <li>Allowed columns: <em>%s</em></li> <li>Existing users are matched by their unique <em>Code</em> property, and updated with any new values from the imported file.</li> <li>Groups can be assigned by the <em>Groups</em> column. Groups are identified by their <em>Code</em> property, multiple groups can be separated by comma. Existing group memberships are not cleared.</li> </ul></div>'
ResultCreated: 'Created {count} members'
ResultCreated: '{count} leden aangemaakt'
ResultDeleted: '%d leden verwijderd'
ResultNone: 'Geen wijzingen'
ResultUpdated: 'Updated {count} members'
@ -551,13 +551,13 @@ nl:
ATTACHFILE: 'Voeg een bestand toe'
ATTACHFILES: 'Voeg bestanden toe'
AttachFile: 'Voeg bestanden toe'
DELETE: 'Delete from files'
DELETE: 'Volledig verwijderen'
DELETEINFO: 'Verwijder dit bestand uit bestandsopslag van de website.'
DOEDIT: Bewaar
DROPFILE: 'Bestand hiernaar toe slepen'
DROPFILES: 'Sleep hier je bestanden'
Dimensions: Dimensions
EDIT: Edit
EDIT: Bewerken
EDITINFO: 'Bewerk dit bestand'
FIELDNOTSET: 'Bestandsinformatie niet gevonden'
FROMCOMPUTER: 'Vanaf computer'

View File

@ -77,7 +77,7 @@ uk:
CHANGEPASSWORDTEXT2: 'Тепер Ви можете використовувати наступні дані для входу:'
EMAIL: Email
HELLO: Привіт
PASSWORD: Password
PASSWORD: Пароль
CheckboxField:
- 'False'
- 'True'
@ -240,7 +240,7 @@ uk:
Saved: 'Saved %s %s'
GridFieldItemEditView.ss: null
Group:
AddRole: 'Add a role for this group'
AddRole: 'Додати роль до цієї групи'
Code: 'Код групи'
DefaultGroupTitleAdministrators: Адміністратори
DefaultGroupTitleContentAuthors: 'Content Authors'
@ -250,7 +250,7 @@ uk:
NoRoles: 'Ролі не знайдені'
PLURALNAME: Groups
Parent: 'Батьківська група'
RolesAddEditLink: 'Manage roles'
RolesAddEditLink: 'Керувати ролями'
SINGULARNAME: Group
Sort: 'Порядок сортування'
has_many_Permissions: Права
@ -501,7 +501,7 @@ uk:
EDITPERMISSIONS_HELP: 'Ability to edit Permissions and IP Addresses for a group. Requires the "Access to ''Security'' section" permission.'
GROUPNAME: 'Назва групи'
IMPORTGROUPS: 'Імпортувати групи'
IMPORTUSERS: 'Import users'
IMPORTUSERS: 'Імпортувати користувачів'
MEMBERS: Члени
MENUTITLE: Security
MemberListCaution: 'Caution: Removing members from this list will remove them from all groups and the database'

View File

@ -76,8 +76,7 @@ class Aggregate extends ViewableData {
* @return SQLQuery
*/
protected function query($attr) {
$singleton = singleton($this->type);
$query = $singleton->buildSQL($this->filter);
$query = DataList::create($this->type)->where($this->filter);
$query->setSelect($attr);
$query->setOrderBy(array());
$singleton->extend('augmentSQL', $query);

View File

@ -66,6 +66,9 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta
* @return ArrayIterator
*/
public function getIterator() {
foreach($this->items as $i => $item) {
if(is_array($item)) $this->items[$i] = new ArrayData($item);
}
return new ArrayIterator($this->items);
}

View File

@ -91,7 +91,7 @@ class DB {
$key = md5($key); // Ensure key is correct length for chosen cypher
$ivSize = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CFB);
$iv = mcrypt_create_iv($ivSize);
$iv = mcrypt_create_iv($ivSize);
$encrypted = mcrypt_encrypt(
MCRYPT_RIJNDAEL_256, $key, $name, MCRYPT_MODE_CFB, $iv
);

View File

@ -664,7 +664,10 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
} else {
$item = Injector::inst()->create($defaultClass, $row, false, $this->model);
}
//set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
$item->setSourceQueryParams($this->dataQuery()->getQueryParams());
return $item;
}

View File

@ -395,9 +395,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/**
* Set the DataModel
* @param DataModel $model
* @return DataObject $this
*/
public function setDataModel(DataModel $model) {
$this->model = $model;
return $this;
}
/**
@ -423,12 +426,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$clone = new $className( $this->toMap(), false, $this->model );
$clone->ID = 0;
$clone->extend('onBeforeDuplicate', $this, $doWrite);
$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite);
if($doWrite) {
$clone->write();
$this->duplicateManyManyRelations($this, $clone);
}
$clone->extend('onAfterDuplicate', $this, $doWrite);
$clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite);
return $clone;
}
@ -503,6 +506,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* or destroy and reinstanciate the record.
*
* @param string $className The new ClassName attribute (a subclass of {@link DataObject})
* @return DataObject $this
*/
public function setClassName($className) {
$className = trim($className);
@ -510,6 +514,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->class = $className;
$this->setField("ClassName", $className);
return $this;
}
/**
@ -745,6 +750,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* the related objects that it alters.
*
* @param array $data A map of field name to data values to update.
* @return DataObject $this
*/
public function update($data) {
foreach($data as $k => $v) {
@ -784,6 +790,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->$k = $v;
}
}
return $this;
}
/**
@ -793,11 +800,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* use the write() method.
*
* @param array $data A map of field name to data values to update.
* @return DataObject $this
*/
public function castedUpdate($data) {
foreach($data as $k => $v) {
$this->setCastedField($k,$v);
}
return $this;
}
/**
@ -895,6 +904,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Forces the record to think that all its data has changed.
* Doesn't write to the database. Only sets fields as changed
* if they are not already marked as changed.
*
* @return DataObject $this
*/
public function forceChange() {
// Ensure lazy fields loaded
@ -913,6 +924,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// @todo Find better way to allow versioned to write a new version after forceChange
if($this->isChanged('Version')) unset($this->changed['Version']);
return $this;
}
/**
@ -987,7 +999,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Will traverse the defaults of the current class and all its parent classes.
* Called by the constructor when creating new records.
*
* @uses DataExtension->populateDefaults()
* @uses DataExtension->populateDefaults()
* @return DataObject $this
*/
public function populateDefaults() {
$classes = array_reverse(ClassInfo::ancestry($this));
@ -1018,6 +1031,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
$this->extend('populateDefaults');
return $this;
}
/**
@ -1065,7 +1079,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if($writeException) {
// Used by DODs to clean up after themselves, eg, Versioned
$this->extend('onAfterSkippedWrite');
$this->invokeWithExtensions('onAfterSkippedWrite');
throw $writeException;
return false;
}
@ -1208,7 +1222,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} elseif ( $showDebug ) {
echo "<b>Debug:</b> no changes for DataObject<br />";
// Used by DODs to clean up after themselves, eg, Versioned
$this->extend('onAfterSkippedWrite');
$this->invokeWithExtensions('onAfterSkippedWrite');
}
// Clears the cache for this object so get_one returns the correct object.
@ -1220,7 +1234,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->record['LastEdited'] = SS_Datetime::now()->Rfc2822();
} else {
// Used by DODs to clean up after themselves, eg, Versioned
$this->extend('onAfterSkippedWrite');
$this->invokeWithExtensions('onAfterSkippedWrite');
}
// Write relations as necessary
@ -1235,13 +1249,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* same record.
*
* @param $recursive Recursively write components
* @return DataObject $this
*/
public function writeComponents($recursive = false) {
if(!$this->components) return;
if(!$this->components) return $this;
foreach($this->components as $component) {
$component->write(false, false, false, $recursive);
}
return $this;
}
/**
@ -1434,7 +1450,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$combinedFilter = "\"$joinField\" = '$id'";
if(!empty($filter)) $combinedFilter .= " AND ({$filter})";
return singleton($componentClass)->extendedSQL($combinedFilter, $sort, $limit, $join);
return DataList::create($componentClass)
->where($combinedFilter)
->canSortBy($sort)
->limit($limit);
}
/**
@ -2100,9 +2119,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Limit query to the current record, unless it has the Versioned extension,
// in which case it requires special handling through augmentLoadLazyFields()
if (!isset($this->record['Version'])){
if(!$this->hasExtension('Versioned')) {
$dataQuery->where("\"$tableClass\".\"ID\" = {$this->record['ID']}")->limit(1);
}
$columns = array();
// Add SQL for fields, both simple & multi-value
@ -2116,7 +2136,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if ($columns) {
$query = $dataQuery->query();
$this->extend('augmentLoadLazyFields', $query, $dataQuery, $this->record);
$this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
$this->extend('augmentSQL', $query, $dataQuery);
$dataQuery->setQueriedColumns($columns);
@ -2227,6 +2247,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*
* @param string $fieldName Name of the field
* @param mixed $val New field value
* @return DataObject $this
*/
public function setField($fieldName, $val) {
// Situation 1: Passing an DBField
@ -2270,6 +2291,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->record[$fieldName] = $val;
}
}
return $this;
}
/**
@ -2280,6 +2302,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*
* @param string $fieldName Name of the field
* @param mixed $value New field value
* @return DataObject $this
*/
public function setCastedField($fieldName, $val) {
if(!$fieldName) {
@ -2293,6 +2316,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} else {
$this->$fieldName = $val;
}
return $this;
}
/**
@ -2829,14 +2853,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*
* @param boolean $persistent When true will also clear persistent data stored in the Cache system.
* When false will just clear session-local cached data
*
* @return DataObject $this
*/
public function flushCache($persistent = true) {
if($persistent) Aggregate::flushCache($this->class);
if($this->class == 'DataObject') {
DataObject::$_cache_get_one = array();
return;
return $this;
}
$classes = ClassInfo::ancestry($this->class);
@ -2847,6 +2871,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->extend('flushCache');
$this->components = array();
return $this;
}
/**
@ -2907,6 +2932,46 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return array_shift($tableClasses);
}
/**
* @var Array Parameters used in the query that built this object.
* This can be used by decorators (e.g. lazy loading) to
* run additional queries using the same context.
*/
protected $sourceQueryParams;
/**
* @see $sourceQueryParams
* @return array
*/
public function getSourceQueryParams() {
return $this->sourceQueryParams;
}
/**
* @see $sourceQueryParams
* @param array
*/
public function setSourceQueryParams($array) {
$this->sourceQueryParams = $array;
}
/**
* @see $sourceQueryParams
* @param array
*/
public function setSourceQueryParam($key, $value) {
$this->sourceQueryParams[$key] = $value;
}
/**
* @see $sourceQueryParams
* @return Mixed
*/
public function getSourceQueryParam($key) {
if(isset($this->sourceQueryParams[$key])) return $this->sourceQueryParams[$key];
else return null;
}
//-------------------------------------------------------------------------------------------//
/**

View File

@ -111,7 +111,7 @@ class DataQuery {
user_error("DataObjects have been requested before the manifest is loaded. Please ensure you are not"
. " querying the database in _config.php.", E_USER_ERROR);
} else {
user_error("DataObject::buildSQL: Can't find data classes (classes linked to tables) for"
user_error("DataList::create Can't find data classes (classes linked to tables) for"
. " $this->dataClass. Please ensure you run dev/build after creating a new DataObject.",
E_USER_ERROR);
}
@ -743,6 +743,14 @@ class DataQuery {
if(isset($this->queryParams[$key])) return $this->queryParams[$key];
else return null;
}
/**
* Returns all query parameters
* @return array query parameters array
*/
public function getQueryParams() {
return $this->queryParams;
}
}
/**

View File

@ -73,15 +73,13 @@ class DatabaseAdmin extends Controller {
/**
* Display a simple HTML menu of database admin helpers.
* When we're called as /dev/build, that's actually the index. Do the same
* as /dev/build/build.
*/
public function index() {
echo "<h2>Database Administration Helpers</h2>";
echo "<p><a href=\"build\">Add missing database fields (similar to sanity check).</a></p>";
echo "<p><a href=\"../images/flush\">Flush <b>all</b> of the generated images.</a></p>";
return $this->build();
}
/**
* Updates the database schema, creating tables & fields as necessary.
*/

View File

@ -210,9 +210,38 @@ class ManyManyList extends RelationList {
}
/**
* @return Array Map of field => fieldtype
* Gets the join table used for the relationship.
*
* @return string the name of the table
*/
function getExtraFields() {
public function getJoinTable() {
return $this->joinTable;
}
/**
* Gets the key used to store the ID of the local/parent object.
*
* @return string the field name
*/
public function getLocalKey() {
return $this->localKey;
}
/**
* Gets the key used to store the ID of the foreign/child object.
*
* @return string the field name
*/
public function getForeignKey() {
return $this->foreignKey;
}
/**
* Gets the extra fields included in the relationship.
*
* @return array a map of field names to types
*/
public function getExtraFields() {
return $this->extraFields;
}

View File

@ -136,7 +136,7 @@ class Versioned extends DataExtension {
* @todo Should this all go into VersionedDataQuery?
*/
public function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery = null) {
$baseTable = ClassInfo::baseDataClass($dataQuery->dataClass());
$baseTable = ClassInfo::baseDataClass($dataQuery->dataClass());
switch($dataQuery->getQueryParam('Versioned.mode')) {
// Noop
@ -277,14 +277,26 @@ class Versioned extends DataExtension {
* For lazy loaded fields requiring extra sql manipulation, ie versioning
* @param SQLQuery $query
* @param DataQuery $dataQuery
* @param array $record
* @param DataObject $dataObject
*/
function augmentLoadLazyFields(SQLQuery &$query, DataQuery &$dataQuery = null, $record) {
function augmentLoadLazyFields(SQLQuery &$query, DataQuery &$dataQuery = null, $dataObject) {
// The VersionedMode local variable ensures that this decorator only applies to
// queries that have originated from the Versioned object, and have the Versioned
// metadata set on the query object. This prevents regular queries from
// accidentally querying the *_versions tables.
$versionedMode = $dataObject->getSourceQueryParam('Versioned.mode');
$dataClass = $dataQuery->dataClass();
if (isset($record['Version'])){
$dataQuery->where("\"$dataClass\".\"RecordID\" = " . $record['ID']);
$dataQuery->where("\"$dataClass\".\"Version\" = " . $record['Version']);
$modesToAllowVersioning = array('all_versions', 'latest_versions', 'archive');
if(
!empty($dataObject->Version) &&
(!empty($versionedMode) && in_array($versionedMode,$modesToAllowVersioning))
) {
$dataQuery->where("\"$dataClass\".\"RecordID\" = " . $dataObject->ID);
$dataQuery->where("\"$dataClass\".\"Version\" = " . $dataObject->Version);
$dataQuery->setQueryParam('Versioned.mode', 'all_versions');
} else {
// Same behaviour as in DataObject->loadLazyFields
$dataQuery->where("\"$dataClass\".\"ID\" = {$dataObject->ID}")->limit(1);
}
}

View File

@ -312,7 +312,7 @@ class Date extends DBField {
* @return boolean
*/
public function InPast() {
return strtotime($this->value) < time();
return strtotime($this->value) < SS_Datetime::now()->Format('U');
}
/**
@ -320,7 +320,7 @@ class Date extends DBField {
* @return boolean
*/
public function InFuture() {
return strtotime($this->value) > time();
return strtotime($this->value) > SS_Datetime::now()->Format('U');
}
/**
@ -328,7 +328,7 @@ class Date extends DBField {
* @return boolean
*/
public function IsToday() {
return (date('Y-m-d', strtotime($this->value)) == date('Y-m-d', time()));
return (date('Y-m-d', strtotime($this->value)) == SS_Datetime::now()->Format('Y-m-d'));
}
/**

View File

@ -136,8 +136,21 @@ class HTMLText extends Text {
return ShortcodeParser::get_active()->parse($this->value);
}
/**
* Returns true if the field has meaningful content.
* Excludes null content like <h1></h1>, <p></p> ,etc
*
* @return boolean
*/
public function exists() {
return parent::exists() && $this->value != '<p></p>';
// If it's blank, it's blank
if(!parent::exists()) return false;
// If it's got a content tag
if(preg_match('/<(img|embed|object|iframe)[^>]*>/i', $this->value)) return true;
// If it's just one or two tags on its own (and not the above) it's empty. This might be <p></p> or <h1></h1> or whatever.
if(preg_match('/^[\\s]*(<[^>]+>[\\s]*){1,2}$/', $this->value)) return false;
// Otherwise its content is genuine content
return true;
}
public function scaffoldFormField($title = null, $params = null) {

View File

@ -32,6 +32,19 @@ class Varchar extends StringField {
parent::__construct($name, $options);
}
/**
* Allow the ability to access the size of the field programatically. This
* can be useful if you want to have text fields with a length limit that
* is dictated by the DB field.
*
* TextField::create('Title')->setMaxLength(singleton('SiteTree')->dbObject('Title')->getSize())
*
* @return int The size of the field
*/
public function getSize() {
return $this->size;
}
/**
* (non-PHPdoc)
* @see DBField::requireField()

View File

@ -2,65 +2,17 @@
/**
* A simple parser that allows you to map BBCode-like "shortcodes" to an arbitrary callback.
*
* * The Shortcode API (new in 2.4) is a simple regex based parser that allows you to replace simple bbcode-like tags
* within a HTMLText or HTMLVarchar field when rendered into a template. It is inspired by and very similar to the
* [Wordpress implementation](http://codex.wordpress.org/Shortcode_API) of shortcodes. Examples of shortcode tags are:
*
* <code>
* [shortcode]
* [shortcode /]
* [shortcode parameter="value"]
* [shortcode parameter="value"]Enclosed Content[/shortcode]
* </code>
*
* <b>Defining Custom Shortcodes</b>
*
* All you need to do to define a shortcode is to register a callback with the parser that will be called whenever a
* shortcode is encountered. This callback will return a string to replace the shortcode with.
*
* To register a shortcode you call:
*
* <code>
* ShortcodeParser::get('default')->register('shortcode_tag_name', 'callback');
* </code>
*
* These parameters are passed to the callback:
* - Any parameters attached to the shortcode as an associative array (keys are lower-case).
* - Any content enclosed within the shortcode (if it is an enclosing shortcode). Note that any content within this
* will not have been parsed, and can optionally be fed back into the parser.
* - The ShortcodeParser instance used to parse the content.
* - The shortcode tag name that was matched within the parsed content.
*
* <b>Inbuilt Shortcodes</b>
*
* From 2.4 onwards links inserted via the CMS into a content field are in the form ''<a href="[sitetree_link id=n]">
* '', and from 3.0 the comma is used as a separator instead ''<a href="[sitetree_link,id=n]">''. At runtime this is
* replaced by a plain link to the page with the ID in question.
*
* <b>Limitations</b>
*
* Since the shortcode parser is based on a simple regular expression it cannot properly handle nested shortcodes. For
* example the below code will not work as expected:
*
* <code>
* [shortcode]
* [shortcode][/shortcode]
* [/shortcode]
* </code>
*
* The parser will recognise this as:
*
* <code>
* [shortcode]
* [shortcode]
* [/shortcode]
* </code>
* See the documentation at docs/reference/shortcodes.md .
*
* @package framework
* @subpackage misc
*/
class ShortcodeParser {
public function img_shortcode($attrs) {
return "<img src='".$attrs['src']."'>";
}
private static $instances = array();
private static $active_instance = 'default';
@ -148,8 +100,362 @@ class ShortcodeParser {
$this->shortcodes = array();
}
public function callShortcode($tag, $attributes, $content) {
if (!isset($this->shortcodes[$tag])) return false;
return call_user_func($this->shortcodes[$tag], $attributes, $content, $this, $tag);
}
// --------------------------------------------------------------------------------------------------------------
protected function removeNode($node) {
$node->parentNode->removeChild($node);
}
protected function insertAfter($new, $after) {
$parent = $after->parentNode; $next = $after->nextSibling;
if ($next) {
$parent->insertBefore($new, $next);
}
else {
$parent->appendChild($new);
}
}
protected function insertListAfter($new, $after) {
$doc = $after->ownerDocument; $parent = $after->parentNode; $next = $after->nextSibling;
for ($i = 0; $i < $new->length; $i++) {
$imported = $doc->importNode($new->item($i), true);
if ($next) {
$parent->insertBefore($imported, $next);
}
else {
$parent->appendChild($imported);
}
}
}
private static $marker_class = '--ss-shortcode-marker';
private static $block_level_elements = array(
'address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'div', 'dl', 'fieldset', 'figcaption',
'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'ol', 'output', 'p',
'pre', 'section', 'table', 'ul'
);
private static $tagrx = '/
<(?<element>(?:"[^"]*"[\'"]*|\'[^\']*\'[\'"]*|[^\'">])+)> | # HTML Tag - skip attribute scoped tags
\[ (?<escaped>\[.*?\]) \] | # Escaped block
\[ (?<open>\w+) (?<attrs>.*?) (?<selfclosed>\/?) \] | # Opening tag
\[\/ (?<close>\w+) \] # Closing tag
/x';
private static $attrrx = '/
([^\s\/\'"=,]+) # Name
\s* = \s*
(?:
(?:\'([^\']+)\') | # Value surrounded by \'
(?:"([^"]+)") | # Value surrounded by "
(\w+) # Bare value
)
/x';
const WARN = 'warn';
const STRIP = 'strip';
const LEAVE = 'leave';
const ERROR = 'error';
public static $error_behavior = self::LEAVE;
/**
* Look through a string that contains shortcode tags and pull out the locations and details
* of those tags
*
* Doesn't support nested shortcode tags
*
* @param string $content
* @return array - The list of tags found. When using an open/close pair, only one item will be in the array,
* with "content" set to the text between the tags
*/
protected function extractTags(&$content) {
$tags = array();
$escapes = array();
if(preg_match_all(self::$tagrx, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
foreach($matches as $match) {
// Record escaped tags
if (!empty($match['escaped'][0])) {
$escapes[] = array(
's' => $match[0][1],
'e' => $match[0][1] + strlen($match[0][0]),
'content' => $match['escaped']
);
}
// Ignore any elements
if (empty($match['open'][0]) && empty($match['close'][0])) continue;
// Pull the attributes out into a key/value hash
$attrs = array();
if (!empty($match['attrs'][0])) {
preg_match_all(self::$attrrx, $match['attrs'][0], $attrmatches, PREG_SET_ORDER);
foreach ($attrmatches as $attr) {
list($whole, $name, $value) = array_values(array_filter($attr));
$attrs[$name] = $value;
}
}
// And store the indexes, tag details, etc
$tags[] = array(
'text' => $match[0][0],
's' => $match[0][1],
'e' => $match[0][1] + strlen($match[0][0]),
'open' => @$match['open'][0],
'close' => @$match['close'][0],
'attrs' => $attrs,
'content' => ''
);
}
}
$i = count($tags);
while($i--) {
if($tags[$i]['close']) {
// If the tag just before this one isn't the related opening tag, throw an error
$err = null;
if ($i == 0) {
$err = 'Close tag "'.$tags[$i]['close'].'" is the first found tag, so has no related open tag';
}
else if (!$tags[$i-1]['open']) {
$err = 'Close tag "'.$tags[$i]['close'].'" preceded by another close tag "'.$tags[$i-1]['close'].'"';
}
else if ($tags[$i]['close'] != $tags[$i-1]['open']) {
$err = 'Close tag "'.$tags[$i]['close'].'" doesn\'t match preceding open tag "'.$tags[$i-1]['open'].'"';
}
if($err) {
if(self::$error_behavior == self::ERROR) user_error($err, E_USER_ERRROR);
}
else {
// Otherwise, grab content between tags, save in opening tag & delete the closing one
$tags[$i-1]['text'] = substr($content, $tags[$i-1]['s'], $tags[$i]['e'] - $tags[$i-1]['s']);
$tags[$i-1]['content'] = substr($content, $tags[$i-1]['e'], $tags[$i]['s'] - $tags[$i-1]['e']);
$tags[$i-1]['e'] = $tags[$i]['e'];
unset($tags[$i]);
}
}
}
$i = count($escapes);
while($i--) {
$escape = $escapes[$i];
$content = substr_replace($content, $escape['content'], $escape['s'], $escape['e'] - $escape['s']);
}
return array_values($tags);
}
/**
* Replaces the shortcode tags extracted by extractTags with HTML element "markers", so that
* we can parse the resulting string as HTML and easily mutate the shortcodes in the DOM
*
* @param string $content - The HTML string with [tag] style shortcodes embedded
* @param array $tags - The tags extracted by extractTags
* @return string - The HTML string with [tag] style shortcodes replaced by markers
*/
protected function replaceTagsWithText($content, $tags, $generator) {
// The string with tags replaced with markers
$str = '';
// The start index of the next tag, remembered as we step backwards through the list
$li = null;
$i = count($tags);
while($i--) {
if ($li === null) $tail = substr($content, $tags[$i]['e']);
else $tail = substr($content, $tags[$i]['e'], $li - $tags[$i]['e']);
$str = $generator($i, $tags[$i]). $tail . $str;
$li = $tags[$i]['s'];
}
return substr($content, 0, $tags[0]['s']) . $str;
}
/**
* Replace the shortcodes in attribute values with the calculated content
*
* We don't use markers with attributes because there's no point, it's easier to do all the matching
* in-DOM after the XML parse
*
* @param DOMDocument $doc
*/
protected function replaceAttributeTagsWithContent($doc) {
$xp = new DOMXPath($doc);
$attributes = $xp->query('//@*[contains(.,"[")][contains(.,"]")]');
$parser = $this;
for($i = 0; $i < $attributes->length; $i++) {
$node = $attributes->item($i);
$tags = $this->extractTags($node->nodeValue);
if($tags) {
$node->nodeValue = $this->replaceTagsWithText($node->nodeValue, $tags, function($idx, $tag) use ($parser){
return $parser->callShortcode($tag['open'], $tag['attrs'], $tag['content']);
});
}
}
}
/**
* Replace the element-scoped tags with markers
*
* @param string $content
*/
protected function replaceElementTagsWithMarkers($content) {
$tags = $this->extractTags($content);
if($tags) {
$markerClass = self::$marker_class;
$content = $this->replaceTagsWithText($content, $tags, function($idx, $tag) use ($markerClass) {
return '<img class="'.$markerClass.'" data-tagid="'.$idx.'" />';
});
}
return array($content, $tags);
}
protected function findParentsForMarkers($nodes) {
$parents = array();
foreach($nodes as $node) {
$parent = $node;
do {
$parent = $parent->parentNode;
}
while($parent instanceof DOMElement && !in_array(strtolower($parent->tagName), self::$block_level_elements));
$node->setAttribute('data-parentid', count($parents));
$parents[] = $parent;
}
return $parents;
}
const BEFORE = 'before';
const AFTER = 'after';
const SPLIT = 'split';
const INLINE = 'inline';
/**
* Given a node with represents a shortcode marker and a location string, mutates the DOM to put the
* marker in the compliant location
*
* For shortcodes inserted BEFORE, that location is just before the block container that
* the marker is in
*
* For shortcodes inserted AFTER, that location is just after the block container that
* the marker is in
*
* For shortcodes inserted SPLIT, that location is where the marker is, but the DOM
* is split around it up to the block container the marker is in - for instance,
*
* <p>A<span>B<marker />C</span>D</p>
*
* becomes
*
* <p>A<span>B</span></p><marker /><p><span>C</span>D</p>
*
* For shortcodes inserted INLINE, no modification is needed (but in that case the shortcode handler needs to
* generate only inline blocks)
*
* @param DOMElement $node
* @param int $location - ShortcodeParser::BEFORE, ShortcodeParser::SPLIT or ShortcodeParser::INLINE
*/
protected function moveMarkerToCompliantHome($node, $parent, $location) {
// Move before block parent
if($location == self::BEFORE) {
$parent->parentNode->insertBefore($node, $parent);
}
// Move after block parent
else if($location == self::AFTER) {
$this->insertAfter($node, $parent);
}
// Split parent at node
else if($location == self::SPLIT) {
$at = $node; $splitee = $node->parentNode;
while($splitee !== $parent->parentNode) {
$spliter = $splitee->cloneNode(false);
$this->insertAfter($spliter, $splitee);
while($at->nextSibling) {
$spliter->appendChild($at->nextSibling);
}
$at = $splitee; $splitee = $splitee->parentNode;
}
$this->insertAfter($node, $parent);
}
// Do nothing
else if($location == self::INLINE) {
if(in_array(strtolower($node->tagName), self::$block_level_elements)) {
user_error(
'Requested to insert block tag '.$node->tagName.' inline - probably this will break HTML compliance',
E_USER_WARNING
);
}
// NOP
}
else {
user_error('Unknown value for $location argument '.$location, E_USER_ERROR);
}
}
/**
* Given a node with represents a shortcode marker and some informationabout the shortcode, call the
* shortcode handler & replace the marker with the actual content
*
* @param DOMElement $node
* @param array $tag
*/
protected function replaceMarkerWithContent($node, $tag) {
$content = false;
if($tag['open']) $content = $this->callShortcode($tag['open'], $tag['attrs'], $tag['content']);
if ($content === false) {
if(self::$error_behavior == self::ERROR) {
user_error('Unknown shortcode tag '.$tag['open'], E_USER_ERRROR);
}
if (self::$error_behavior == self::WARN) {
$content = '<strong class="warning">'.$tag['text'].'</strong>';
}
else if (self::$error_behavior == self::LEAVE) {
$content = $tag['text'];
}
else {
// self::$error_behavior == self::STRIP - NOP
}
}
if ($content) {
$parsed = HTML5_Parser::parseFragment($content, 'div');
$this->insertListAfter($parsed, $node);
}
$this->removeNode($node);
}
/**
* Parse a string, and replace any registered shortcodes within it with the result of the mapped callback.
*
@ -157,56 +463,73 @@ class ShortcodeParser {
* @return string
*/
public function parse($content) {
// If no shortcodes defined, don't try and parse any
if(!$this->shortcodes) return $content;
$shortcodes = implode('|', array_map('preg_quote', array_keys($this->shortcodes)));
$pattern = "/\[($shortcodes)(.*?)(\/\]|\](?(4)|(?:(.+?)\[\/\s*\\1\s*\]))|\])/s";
if(preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
$replacements = array();
foreach($matches as $match) {
$prefix = $match[0][1] ? $content[$match[0][1]-1] : '';
if(strlen($match[0][0]) + $match[0][1] < strlen($content)) {
$suffix = $content[strlen($match[0][0]) + $match[0][1]];
} else {
$suffix = '';
}
if($prefix == '[' && $suffix == ']') {
$replacements[] = array($match[0][0], $match[0][1]-1, strlen($match[0][0]) + 2);
} else {
$replacements[] = array($this->handleShortcode($match), $match[0][1], strlen($match[0][0]));
}
// If no content, don't try and parse it
if (!trim($content)) return $content;
// First we operate in text mode, replacing any shortcodes with marker elements so that later we can
// use a proper DOM
list($content, $tags) = $this->replaceElementTagsWithMarkers($content);
// Now parse the result into a DOM
require_once(THIRDPARTY_PATH.'/html5lib/HTML5/Parser.php');
$bases = HTML5_Parser::parseFragment(trim($content), 'div');
// If we couldn't parse the HTML, error out
if (!$bases || !$bases->length) {
if(self::$error_behavior == self::ERROR) {
user_error('Couldn\'t decode HTML when processing short codes', E_USER_ERRROR);
}
// We reverse this so that replacements don't break offsets
foreach(array_reverse($replacements) as $replace) {
$content = substr_replace($content, $replace[0], $replace[1], $replace[2]);
else {
return $content;
}
}
$res = '';
$html = $bases->item(0)->parentNode;
$doc = $html->ownerDocument;
$xp = new DOMXPath($doc);
// First, replace any shortcodes that are in attributes
$this->replaceAttributeTagsWithContent($doc);
// Find all the element scoped shortcode markers
$shortcodes = $xp->query('//img[@class="'.self::$marker_class.'"]');
// Find the parents. Do this before DOM modification, since SPLIT might cause parents to move otherwise
$parents = $this->findParentsForMarkers($shortcodes);
return $content;
foreach($shortcodes as $shortcode) {
$tag = $tags[$shortcode->getAttribute('data-tagid')];
$parent = $parents[$shortcode->getAttribute('data-parentid')];
$class = null;
if(!empty($tag['attrs']['location'])) $class = $tag['attrs']['location'];
else if(!empty($tag['attrs']['class'])) $class = $tag['attrs']['class'];
$location = self::INLINE;
if($class == 'left' || $class == 'right') $location = self::BEFORE;
if($class == 'center' || $class == 'leftALone') $location = self::SPLIT;
if(!$parent) {
if($location !== self::INLINE) {
user_error("Parent block for shortcode couldn't be found, but location wasn't INLINE", E_USER_ERROR);
}
}
else {
$this->moveMarkerToCompliantHome($shortcode, $parent, $location);
}
$this->replaceMarkerWithContent($shortcode, $tag);
}
foreach($html->childNodes as $child) $res .= $doc->saveHTML($child);
return $res;
}
/**
* @ignore
*/
protected function handleShortcode($matches) {
$shortcode = $matches[1][0];
$attributes = array(); // Parse attributes into into this array.
if(preg_match_all('/(\w+) *= *(?:([\'"])(.*?)\\2|([^ ,"\'>]+))/', $matches[2][0], $match, PREG_SET_ORDER)) {
foreach($match as $attribute) {
if(!empty($attribute[4])) {
$attributes[strtolower($attribute[1])] = $attribute[4];
} elseif(!empty($attribute[3])) {
$attributes[strtolower($attribute[1])] = $attribute[3];
}
}
}
return call_user_func(
$this->shortcodes[$shortcode],
$attributes, isset($matches[4][0]) ? $matches[4][0] : '', $this, $shortcode);
}
}

View File

@ -27,6 +27,12 @@
padding-left: 12px;
}
#Form_EditorToolbarMediaForm {
.ui-tabs-panel {
padding-left: 0px;
}
}
body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fieldset {
padding: $grid-x*2;
overflow: auto;
@ -135,7 +141,7 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
.ss-uploadfield-item-info {
position: relative;
line-height: 30px;
font-size: 18px;
font-size: 14px;
overflow: hidden;
background-color: #5db4df;
@include background-image(linear-gradient(top, #5db4df 0%,#5db1dd 8%,#439bcb 50%,#3f99cd 54%,#207db6 96%,#1e7cba 100%));
@ -227,7 +233,8 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
left: 0;
z-index: 0;
color: #f00;
@include ss-uploadfield-action-buttons;
@include ss-uploadfield-action-buttons;
font-size:14px;
}
.ss-uploadfield-item-progress {
@ -267,20 +274,27 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
.ss-uploadfield-item-info {
float: left;
margin: 34px 0 0;
margin: 10px 0 0;
.ss-insert-media &{
margin: 15px 0px 0 20px;
margin: 10px 0px 0 20px;
}
label{
font-size: 16px;
font-size: 18px;
line-height: 30px;
padding: 5px 16px;
padding: 8px 16px;
margin-right:0px;
}
}
.ss-uploadfield-fromcomputer {
/*position: relative; */
overflow: hidden;
display: block;
.btn-icon-drive-upload-large {
background: url(../images/drive-upload-large.png) no-repeat 0px -4px;
width:32px;
height:32px;
margin-top:-12px;
}
}
.ss-uploadfield-item-uploador {
float: left;
@ -288,7 +302,7 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
font-size: 22px;
padding: 0 20px;
line-height: 70px;
margin-top:16px;
margin-top:4px;
display: none;
.ss-insert-media &{
font-size: 18px;
@ -302,9 +316,10 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
border: 2px dashed $color-medium-separator;
background: lighten($color-base,12%);
display: none;
height: 82px;
height: 54px;
width: 360px;
float: left;
text-align: center;
&.active{
&.hover{
@ -314,32 +329,31 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
div {
color:lighten($color-text, 10%);
text-shadow: 0px 1px 0px #fff;
background: url('../images/upload.png') 0 25px no-repeat;
width:230px;
background: url('../images/upload.png') 0 12px no-repeat;
z-index:1;
padding: 20px 0 0;
padding: 20px 38px 0;
line-height: 25px;
font-size: 25px;
font-size: 20px;
font-weight: bold;
text-align: center;
display:block;
display: inline-block;
margin:0 auto;
span {
display: block;
font-size: 14px;
font-size: 12px;
z-index:-1;
margin-top:-3px;
}
}
.ss-insert-media &{
margin-top:3px;
height: 56px; //Design has these smaller than main upload area
width: 277px;
height: 54px;
width: 277px; //Design has these smaller than main upload area
overflow:hidden;
div{
background-position:0 15px;
background-position:0 13px;
padding-top:22px;
span{
height:30px;
height:38px;
font-size:18px;
line-height: 18px;
}

View File

@ -3,12 +3,4 @@
padding: 0;
clear: none;
float: left;
}
.datetime .date .middleColumn {
width: 20em;
}
.datetime .time .middleColumn {
width: 10em;
}

View File

@ -593,6 +593,8 @@ $gf_grid_x: 16px;
border:none;
width:10px;
margin:0 10px;
display:inline;
float:none;
span {
text-indent:-9999em;
}

View File

@ -7,7 +7,7 @@
//*Mixin generates the generic button styling for the gridfield*/
@mixin gridFieldButtons{
border:none;
border:none;
display:block;
text-indent:-9999em;
width:30px;
@ -32,7 +32,7 @@
&.ss-uploadfield-item-cancel{
@include border-radius(0);
border-left:1px solid rgba(#fff, 0.2);
margin-top:3px;
margin-top:0px;
cursor: pointer;
opacity:0.9;
&:hover{
@ -42,9 +42,9 @@
display: block;
margin: 0;
position:realtive;
top:4px;
}
}
top:8px;
}
}
@include ss-uploadfield-editButton;
}
}
@ -57,12 +57,12 @@
@mixin ss-uploadfield-editButton{
&.ss-uploadfield-item-edit {
opacity:0.9;
padding-top: 3px;
padding-top: 1px;
padding-bottom: 0;
height:100%;
@include border-radius(0);
&.ui-state-hover{
background:none;
background:none;
opacity:1;
span.toggle-details{
opacity:1;
@ -81,7 +81,7 @@
width: 8px;
height: 8px;
vertical-align: middle;
&.opened {
&.opened {
margin-top:0;
}
}
@ -90,5 +90,5 @@
.ui-icon {
display: none;
}
}
}

View File

@ -373,7 +373,7 @@ class Group extends DataObject {
// without this check, a user would be able to add himself to an administrators group
// with just access to the "Security" admin interface
Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin") &&
!DataObject::get("Permission", "GroupID = $this->ID AND Code = 'ADMIN'")
!Permission::get()->filter(array('GroupID' => $this->ID, 'Code' => 'ADMIN'))->exists()
)
) {
return true;

View File

@ -383,9 +383,11 @@ class Member extends DataObject implements TemplateGlobalProvider {
$member = DataObject::get_one("Member", "\"Member\".\"ID\" = '$SQL_uid'");
// check if autologin token matches
$hash = $member->encryptWithUserSettings($token);
if($member && (!$member->RememberLoginToken || $member->RememberLoginToken != $hash)) {
$member = null;
if($member) {
$hash = $member->encryptWithUserSettings($token);
if(!$member->RememberLoginToken || $member->RememberLoginToken !== $hash) {
$member = null;
}
}
if($member) {
@ -420,9 +422,12 @@ class Member extends DataObject implements TemplateGlobalProvider {
$this->extend('memberLoggedOut');
$this->RememberLoginToken = null;
Cookie::set('alc_enc', null);
Cookie::set('alc_enc', null); // // Clear the Remember Me cookie
Cookie::forceExpiry('alc_enc');
// Switch back to live in order to avoid infinite loops when redirecting to the login screen (if this login screen is versioned)
Session::clear('readingMode');
$this->write();
// Audit logging hook

View File

@ -138,14 +138,10 @@ JS
if($backURL) Session::set('BackURL', $backURL);
if($badLoginURL = Session::get("BadLoginURL")) {
$this->controller->redirect($badLoginURL);
} else {
// Show the right tab on failed login
$loginLink = Director::absoluteURL($this->controller->Link('login'));
if($backURL) $loginLink .= '?BackURL=' . urlencode($backURL);
$this->controller->redirect($loginLink . '#' . $this->FormName() .'_tab');
}
// Show the right tab on failed login
$loginLink = Director::absoluteURL($this->controller->Link('login'));
if($backURL) $loginLink .= '?BackURL=' . urlencode($backURL);
$this->controller->redirect($loginLink . '#' . $this->FormName() .'_tab');
}
}

View File

@ -0,0 +1,10 @@
<?php
/**
* Throw this exception to register that a user doesn't have permission to do the given action
* and potentially redirect them to the log-in page. The exception message may be presented to the
* user, so it shouldn't be in nerd-speak.
*/
class PermissionFailureException extends Exception {
}

View File

@ -242,7 +242,10 @@ class Security extends Controller {
// Audit logging hook
$controller->extend('permissionDenied', $member);
$controller->redirect("Security/login?BackURL=" . urlencode($_SERVER['REQUEST_URI']));
$controller->redirect(
Config::inst()->get('Security', 'login_url')
. "?BackURL=" . urlencode($_SERVER['REQUEST_URI'])
);
}
return;
}
@ -344,7 +347,6 @@ class Security extends Controller {
}
}
$customCSS = project() . '/css/tabs.css';
if(Director::fileExists($customCSS)) {
Requirements::css($customCSS);
@ -360,11 +362,12 @@ class Security extends Controller {
$controller = Page_Controller::create($tmpPage);
$controller->setDataModel($this->model);
$controller->init();
//Controller::$currentController = $controller;
} else {
$controller = $this;
}
// if the controller calls Director::redirect(), this will break early
if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
$content = '';
$forms = $this->GetLoginForms();
@ -458,6 +461,9 @@ class Security extends Controller {
$controller = $this;
}
// if the controller calls Director::redirect(), this will break early
if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
$customisedController = $controller->customise(array(
'Content' =>
'<p>' .
@ -517,6 +523,9 @@ class Security extends Controller {
$controller = $this;
}
// if the controller calls Director::redirect(), this will break early
if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
$email = Convert::raw2xml(rawurldecode($request->param('ID')) . '.' . $request->getExtension());
$customisedController = $controller->customise(array(
@ -580,6 +589,9 @@ class Security extends Controller {
$controller = $this;
}
// if the controller calls Director::redirect(), this will break early
if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
// Extract the member from the URL.
$member = null;
if (isset($_REQUEST['m'])) {
@ -817,17 +829,8 @@ class Security extends Controller {
* @see set_password_encryption_algorithm()
*/
public static function encrypt_password($password, $salt = null, $algorithm = null, $member = null) {
if(
// if the password is empty, don't encrypt
strlen(trim($password)) == 0
// if no algorithm is provided and no default is set, don't encrypt
|| (!$algorithm)
) {
$algorithm = 'none';
} else {
// Fall back to the default encryption algorithm
if(!$algorithm) $algorithm = self::$encryptionAlgorithm;
}
// Fall back to the default encryption algorithm
if(!$algorithm) $algorithm = self::$encryptionAlgorithm;
$e = PasswordEncryptor::create_for_algorithm($algorithm);
@ -927,8 +930,25 @@ class Security extends Controller {
public static function set_ignore_disallowed_actions($flag) {
self::$ignore_disallowed_actions = $flag;
}
public static function ignore_disallowed_actions() {
return self::$ignore_disallowed_actions;
}
protected static $login_url = "Security/login";
/**
* Set a custom log-in URL if you have built your own log-in page.
*/
public static function set_login_url($loginUrl) {
self::$login_url = $loginUrl;
}
/**
* Get the URL of the log-in page.
* Defaults to Security/login but can be re-set with {@link set_login_url()}
*/
public static function login_url() {
return self::$login_url;
}
}

View File

@ -39,7 +39,7 @@
*
* <code>
* # Quarter-hourly task (every hour at 25 minutes past) (remove space between first * and /15)
* * /15 * * * * www-data /webroot/framework/cli-script.php /QuarterlyHourlyTask > /var/log/quarterhourlytask.log
* * /15 * * * * www-data /webroot/framework/cli-script.php /QuarterHourlyTask > /var/log/quarterhourlytask.log
*
* # HourlyTask (every hour at 25 minutes past)
* 25 * * * * www-data /webroot/framework/cli-script.php /HourlyTask > /var/log/hourlytask.log

View File

@ -10,7 +10,7 @@
<div class="ss-uploadfield-item-info">
<label class="ss-uploadfield-fromcomputer ss-ui-button ss-ui-action-constructive" title="<% _t('AssetUploadField.FROMCOMPUTERINFO', 'Upload from your computer') %>" data-icon="drive-upload">
<label class="ss-uploadfield-fromcomputer ss-ui-button ss-ui-action-constructive" title="<% _t('AssetUploadField.FROMCOMPUTERINFO', 'Upload from your computer') %>" data-icon="drive-upload-large">
<% _t('AssetUploadField.TOUPLOAD', 'Choose files to upload...') %>
<input id="$id" name="$getName" class="$extraClass ss-uploadfield-fromcomputer-fileinput" data-config="$configString" type="file"<% if $multiple %> multiple="multiple"<% end_if %> title="<% _t('AssetUploadField.FROMCOMPUTER', 'Choose files from your computer') %>" />
</label>

View File

@ -23,7 +23,7 @@
<p>To get started with the SilverStripe framework:</p>
<ol>
<li>Create a <code>Controller</code> subclass (<a href="http://doc.silverstripe.org/framework/en/topics/controller">doc.silverstripe.org/framework/en/topics/controller</a>)</li>
<li>Setup the routes.yml f to your <code>Controller</code> (<a href="http://doc.silverstripe.org/framework/en/reference/director#routing">doc.silverstripe.org/framework/en/reference/director#routing</a>).</li>
<li>Setup the routes.yml to your <code>Controller</code> (<a href="http://doc.silverstripe.org/framework/en/reference/director#routing">doc.silverstripe.org/framework/en/reference/director#routing</a>).</li>
<li>Create a template for your <code>Controller</code> (<a href="http://doc.silverstripe.org/framework/en/reference/templates">doc.silverstripe.org/framework/en/reference/templates</a>)</li>
</ol>

View File

@ -8,7 +8,7 @@
</h4>
<div class="ss-uploadfield-item-info">
<label class="ss-uploadfield-fromcomputer ss-ui-button ss-ui-action-constructive" title="<% _t('AssetUploadField.FROMCOMPUTERINFO', 'Upload from your computer') %>" data-icon="drive-upload">
<label class="ss-uploadfield-fromcomputer ss-ui-button ss-ui-action-constructive" title="<% _t('AssetUploadField.FROMCOMPUTERINFO', 'Upload from your computer') %>" data-icon="drive-upload-large">
<% _t('AssetUploadField.TOUPLOAD', 'Choose files to upload...') %>
<input id="$id" name="$getName" class="$extraClass ss-uploadfield-fromcomputer-fileinput" data-config="$configString" type="file"<% if $multiple %> multiple="multiple"<% end_if %> title="<% _t('AssetUploadField.FROMCOMPUTER', 'Choose files from your computer') %>" />
</label>

View File

@ -26,7 +26,7 @@
<div class="ss-uploadfield-item-cancel cancel">
<button data-icon="deleteLight" class="ss-uploadfield-item-cancel ss-uploadfield-item-remove" title="<% _t('UploadField.REMOVE', 'Remove') %>">
<% _t('UploadField.REMOVE', 'Remove') %>
</button></div>
</button>
<div class="ss-uploadfield-item-edit edit">
<button class="ss-uploadfield-item-edit ss-ui-button ui-corner-all" title="<% _t('UploadField.EDITINFO', 'Edit this file') %>" data-icon="pencil">

View File

@ -1,4 +1,9 @@
<form $FormAttributes>
<% if Message %>
<p id="{$FormName}_error" class="message $MessageType">$Message</p>
<% else %>
<p id="{$FormName}_error" class="message $MessageType" style="display: none"></p>
<% end_if %>
<fieldset>
<% loop Fields %>
$FieldHolder

View File

@ -1,5 +1,5 @@
<select $AttributesHTML>
<% loop Options %>
<option value="$Value"<% if Selected %> selected="selected"<% end_if %><% if Disabled %> disabled="disabled"<% end_if %>>$Title</option>
<option value="$Value.XML"<% if Selected %> selected="selected"<% end_if %><% if Disabled %> disabled="disabled"<% end_if %>>$Title.XML</option>
<% end_loop %>
</select>

View File

@ -145,16 +145,55 @@ class RestfulServiceTest extends SapphireTest {
/**
* Simulate cached response file for testing error requests that are supposed to have cache files
*
* @todo Generate the cachepath without hardcoding the cache data
*/
private function createFakeCachedResponse($connection, $subUrl) {
$fullUrl = $connection->getAbsoluteRequestURL($subUrl);
$cachedir = TEMP_FOLDER; // Default silverstripe cache
$cache_file = md5($fullUrl); // Encoded name of cache file
$cache_path = $cachedir."/xmlresponse_$cache_file";
//these are the defaul values that one would expect in the
$basicAuthStringMethod = new ReflectionMethod('RestfulServiceTest_MockErrorService', 'getBasicAuthString');
$basicAuthStringMethod->setAccessible(true);
$cachePathMethod = new ReflectionMethod('RestfulServiceTest_MockErrorService', 'getCachePath');
$cachePathMethod->setAccessible(true);
$cache_path = $cachePathMethod->invokeArgs($connection, array(array(
$fullUrl,
'GET',
null,
array(),
array(),
$basicAuthStringMethod->invoke($connection)
)));
$cacheResponse = new RestfulService_Response("Cache response body");
$store = serialize($cacheResponse);
file_put_contents($cache_path, $store);
}
public function testHttpHeaderParseing() {
$headers = "content-type: text/html; charset=UTF-8\r\n".
"Server: Funky/1.0\r\n".
"Set-Cookie: foo=bar\r\n".
"Set-Cookie: baz=quux\r\n".
"Set-Cookie: bar=foo\r\n";
$expected = array(
'Content-Type' => 'text/html; charset=UTF-8',
'Server' => 'Funky/1.0',
'Set-Cookie' => array(
'foo=bar',
'baz=quux',
'bar=foo'
)
);
$headerFunction = new ReflectionMethod('RestfulService', 'parseRawHeaders');
$headerFunction->setAccessible(true);
$this->assertEquals(
$expected,
$headerFunction->invoke(
new RestfulService(Director::absoluteBaseURL(),0), $headers
)
);
}
}
class RestfulServiceTest_Controller extends Controller implements TestOnly {

View File

@ -32,13 +32,12 @@ class XMLDataFormatterTest extends SapphireTest {
$this->assertEquals('Test Company', (string) $xml->Company);
$this->assertEquals($obj->ID, (int) $xml->ID);
$this->assertEquals(
'<Content><![CDATA[<a href="http://mysite.com">mysite.com</a> is a link in this HTML content.'
. ' <![CDATA[this is some nested CDATA]]]]><![CDATA[>]]></Content>',
'<Content><![CDATA[<a href="http://mysite.com">mysite.com</a> is a link in this HTML content.]]>'
. '</Content>',
$xml->Content->asXML()
);
$this->assertEquals(
'<a href="http://mysite.com">mysite.com</a> is a link in this HTML content.'
. ' <![CDATA[this is some nested CDATA]]>',
'<a href="http://mysite.com">mysite.com</a> is a link in this HTML content.',
(string)$xml->Content
);
}

View File

@ -2,4 +2,4 @@ XMLDataFormatterTest_DataObject:
test-do:
Name: Test DataObject
Company: Test Company
Content: <a href="http://mysite.com">mysite.com</a> is a link in this HTML content. <![CDATA[this is some nested CDATA]]>
Content: <a href="http://mysite.com">mysite.com</a> is a link in this HTML content.

View File

@ -5,6 +5,12 @@ class ControllerTest extends FunctionalTest {
static $fixture_file = 'ControllerTest.yml';
protected $autoFollowRedirection = false;
protected $requiredExtensions = array(
'ControllerTest_AccessBaseController' => array(
'ControllerTest_AccessBaseControllerExtension'
)
);
public function testDefaultAction() {
/* For a controller with a template, the default action will simple run that template. */
@ -31,57 +37,143 @@ class ControllerTest extends FunctionalTest {
}
public function testUndefinedActions() {
$response = Director::test('ControllerTest_UnsecuredController/undefinedaction');
$response = Director::test('ControllerTest_AccessUnsecuredSubController/undefinedaction');
$this->assertEquals(404, $response->getStatusCode(), 'Undefined actions return a not found response.');
}
public function testAllowedActions() {
$adminUser = $this->objFromFixture('Member', 'admin');
$response = $this->get("ControllerTest_SecuredController/methodaction");
$this->assertEquals(200, $response->getStatusCode());
$response = $this->get("ControllerTest_SecuredController/stringaction");
$this->assertEquals(404, $response->getStatusCode());
$response = $this->get("ControllerTest_SecuredController/adminonly");
$this->assertEquals(403, $response->getStatusCode());
$response = $this->get('ControllerTest_UnsecuredController/stringaction');
$this->assertEquals(200, $response->getStatusCode(),
"test that a controller without a specified allowed_actions allows actions through"
);
$response = $this->get("ControllerTest_FullSecuredController/index");
$this->assertEquals(403, $response->getStatusCode(),
"Actions can be globally disallowed by using asterisk (*) for index method"
);
$response = $this->get("ControllerTest_FullSecuredController/adminonly");
$this->assertEquals(404, $response->getStatusCode(),
"Actions can be globally disallowed by using asterisk (*) instead of a method name"
);
$response = $this->get("ControllerTest_FullSecuredController/unsecuredaction");
$response = $this->get("ControllerTest_UnsecuredController/");
$this->assertEquals(200, $response->getStatusCode(),
"Actions can be overridden to be allowed if globally disallowed by using asterisk (*)"
'Access granted on index action without $allowed_actions on defining controller, ' .
'when called without an action in the URL'
);
$response = $this->get("ControllerTest_UnsecuredController/index");
$this->assertEquals(200, $response->getStatusCode(),
'Access granted on index action without $allowed_actions on defining controller, ' .
'when called with an action in the URL'
);
$response = $this->get("ControllerTest_UnsecuredController/method1");
$this->assertEquals(200, $response->getStatusCode(),
'Access granted on action without $allowed_actions on defining controller, ' .
'when called without an action in the URL'
);
$response = $this->get("ControllerTest_AccessBaseController/");
$this->assertEquals(200, $response->getStatusCode(),
'Access granted on index with empty $allowed_actions on defining controller, ' .
'when called without an action in the URL'
);
$response = $this->get("ControllerTest_AccessBaseController/index");
$this->assertEquals(200, $response->getStatusCode(),
'Access granted on index with empty $allowed_actions on defining controller, ' .
'when called with an action in the URL'
);
$response = $this->get("ControllerTest_AccessBaseController/method1");
$this->assertEquals(403, $response->getStatusCode(),
'Access denied on action with empty $allowed_actions on defining controller'
);
$response = $this->get("ControllerTest_AccessBaseController/method2");
$this->assertEquals(403, $response->getStatusCode(),
'Access denied on action with empty $allowed_actions on defining controller, ' .
'even when action is allowed in subclasses (allowed_actions don\'t inherit)'
);
$response = $this->get("ControllerTest_AccessSecuredController/");
$this->assertEquals(200, $response->getStatusCode(),
'Access granted on index with non-empty $allowed_actions on defining controller, ' .
'even when index isn\'t specifically mentioned in there'
);
$response = $this->get("ControllerTest_AccessSecuredController/method1");
$this->assertEquals(403, $response->getStatusCode(),
'Access denied on action which is only defined in parent controller, ' .
'even when action is allowed in currently called class (allowed_actions don\'t inherit)'
);
$response = $this->get("ControllerTest_AccessSecuredController/method2");
$this->assertEquals(200, $response->getStatusCode(),
'Access grante on action originally defined with empty $allowed_actions on parent controller, ' .
'because it has been redefined in the subclass'
);
$response = $this->get("ControllerTest_AccessSecuredController/adminonly");
$this->assertEquals(403, $response->getStatusCode(),
'Access denied on action with $allowed_actions on defining controller, ' .
'when restricted by unmatched permission code'
);
$response = $this->get("ControllerTest_AccessSecuredController/aDmiNOnlY");
$this->assertEquals(403, $response->getStatusCode(),
'Access denied on action with $allowed_actions on defining controller, ' .
'regardless of capitalization'
);
$response = $this->get('ControllerTest_AccessSecuredController/protectedmethod');
$this->assertEquals(404, $response->getStatusCode(),
"Access denied to protected method even if its listed in allowed_actions"
);
$this->session()->inst_set('loggedInAs', $adminUser->ID);
$response = $this->get("ControllerTest_SecuredController/adminonly");
$this->assertEquals(
200,
$response->getStatusCode(),
$response = $this->get("ControllerTest_AccessSecuredController/adminonly");
$this->assertEquals(200, $response->getStatusCode(),
"Permission codes are respected when set in \$allowed_actions"
);
$this->session()->inst_set('loggedInAs', null);
$response = $this->get("ControllerTest_FullSecuredController/adminonly");
$this->assertEquals(200, $response->getStatusCode(),
"Actions can be globally disallowed by using asterisk (*) instead of a method name"
$response = $this->get('ControllerTest_AccessBaseController/extensionmethod1');
$this->assertEquals(200, $response->getStatusCode(),
"Access granted to method defined in allowed_actions on extension, " .
"where method is also defined on extension"
);
$response = $this->get('ControllerTest_AccessSecuredController/extensionmethod1');
$this->assertEquals(200, $response->getStatusCode(),
"Access granted to method defined in allowed_actions on extension, " .
"where method is also defined on extension, even when called in a subclass"
);
$response = $this->get('ControllerTest_AccessBaseController/extensionmethod2');
$this->assertEquals(404, $response->getStatusCode(),
"Access denied to method not defined in allowed_actions on extension, " .
"where method is also defined on extension"
);
$response = $this->get('ControllerTest_IndexSecuredController/');
$this->assertEquals(403, $response->getStatusCode(),
"Access denied when index action is limited through allowed_actions, " .
"and doesn't satisfy checks, and action is empty"
);
$response = $this->get('ControllerTest_IndexSecuredController/index');
$this->assertEquals(403, $response->getStatusCode(),
"Access denied when index action is limited through allowed_actions, " .
"and doesn't satisfy checks"
);
$this->session()->inst_set('loggedInAs', $adminUser->ID);
$response = $this->get('ControllerTest_IndexSecuredController/');
$this->assertEquals(200, $response->getStatusCode(),
"Access granted when index action is limited through allowed_actions, " .
"and does satisfy checks"
);
$this->session()->inst_set('loggedInAs', null);
}
/**
* @expectedException PHPUnit_Framework_Error
* @expectedExceptionMessage Wildcards (*) are no longer valid
*/
public function testWildcardAllowedActions() {
$this->get('ControllerTest_AccessWildcardSecuredController');
}
/**
* Test Controller::join_links()
*/
@ -133,20 +225,60 @@ class ControllerTest extends FunctionalTest {
*/
public function testHasAction() {
$controller = new ControllerTest_HasAction();
$unsecuredController = new ControllerTest_HasAction_Unsecured();
$securedController = new ControllerTest_AccessSecuredController();
$this->assertFalse($controller->hasAction('1'), 'Numeric actions do not slip through.');
//$this->assertFalse($controller->hasAction('lowercase_permission'),
//'Lowercase permission does not slip through.');
$this->assertFalse($controller->hasAction('undefined'), 'undefined actions do not exist');
$this->assertTrue($controller->hasAction('allowed_action'), 'allowed actions are recognised');
$this->assertTrue($controller->hasAction('template_action'), 'action-specific templates are recognised');
$unsecured = new ControllerTest_HasAction_Unsecured();
$this->assertFalse(
$controller->hasAction('1'),
'Numeric actions do not slip through.'
);
//$this->assertFalse(
// $controller->hasAction('lowercase_permission'),
// 'Lowercase permission does not slip through.'
//);
$this->assertFalse(
$controller->hasAction('undefined'),
'undefined actions do not exist'
);
$this->assertTrue(
$controller->hasAction('allowed_action'),
'allowed actions are recognised'
);
$this->assertTrue(
$controller->hasAction('template_action'),
'action-specific templates are recognised'
);
$this->assertTrue (
$unsecured->hasAction('defined_action'),
$unsecuredController->hasAction('defined_action'),
'Without an allowed_actions, any defined methods are recognised as actions'
);
$this->assertTrue(
$securedController->hasAction('adminonly'),
'Method is generally visible even if its denied via allowed_actions'
);
$this->assertFalse(
$securedController->hasAction('protectedmethod'),
'Method is not visible when protected, even if its defined in allowed_actions'
);
$this->assertTrue(
$securedController->hasAction('extensionmethod1'),
'Method is visible when defined on an extension and part of allowed_actions'
);
$this->assertFalse(
$securedController->hasAction('internalextensionmethod'),
'Method is not visible when defined on an extension, but not part of allowed_actions'
);
$this->assertFalse(
$securedController->hasAction('protectedextensionmethod'),
'Method is not visible when defined on an extension, part of allowed_actions, ' .
'but with protected visibility'
);
}
/* Controller::BaseURL no longer exists, but was just a direct call to Director::BaseURL, so not sure what this
@ -209,7 +341,14 @@ class ControllerTest extends FunctionalTest {
* Simple controller for testing
*/
class ControllerTest_Controller extends Controller implements TestOnly {
public $Content = "default content";
public static $allowed_actions = array(
'methodaction',
'stringaction',
'redirectbacktest',
);
public function methodaction() {
return array(
@ -226,49 +365,83 @@ class ControllerTest_Controller extends Controller implements TestOnly {
}
}
/**
* Controller with an $allowed_actions value
*/
class ControllerTest_SecuredController extends Controller implements TestOnly {
class ControllerTest_UnsecuredController extends Controller implements TestOnly {
// Not defined, allow access to all
// static $allowed_actions = array();
// Granted for all
public function method1() {}
// Granted for all
public function method2() {}
}
class ControllerTest_AccessBaseController extends Controller implements TestOnly {
static $allowed_actions = array();
// Denied for all
public function method1() {}
// Denied for all
public function method2() {}
}
class ControllerTest_AccessSecuredController extends ControllerTest_AccessBaseController implements TestOnly {
static $allowed_actions = array(
"methodaction",
"method1", // denied because only defined in parent
"method2" => true, // granted because its redefined
"adminonly" => "ADMIN",
"protectedmethod" => true, // denied because its protected
);
public $Content = "default content";
public function methodaction() {
return array(
"Content" => "methodaction content"
);
}
public function stringaction() {
return "stringaction was called.";
}
public function adminonly() {
return "You must be an admin!";
}
public function method2() {}
public function adminonly() {}
protected function protectedmethod() {}
}
class ControllerTest_FullSecuredController extends Controller implements TestOnly {
class ControllerTest_AccessWildcardSecuredController extends ControllerTest_AccessBaseController implements TestOnly {
static $allowed_actions = array(
"*" => "ADMIN",
'unsecuredaction' => true,
"*" => "ADMIN", // should throw exception
);
public function adminonly() {
return "You must be an admin!";
}
public function unsecuredaction() {
return "Allowed for everybody";
}
}
class ControllerTest_UnsecuredController extends ControllerTest_SecuredController implements TestOnly {}
class ControllerTest_IndexSecuredController extends ControllerTest_AccessBaseController implements TestOnly {
static $allowed_actions = array(
"index" => "ADMIN",
);
}
class ControllerTest_AccessBaseControllerExtension extends Extension implements TestOnly {
static $allowed_actions = array(
"extensionmethod1" => true, // granted because defined on this class
"method1" => true, // ignored because method not defined on this class
"method2" => true, // ignored because method not defined on this class
"protectedextensionmethod" => true, // ignored because method is protected
);
// Allowed for all
public function extensionmethod1() {}
// Denied for all, not defined
public function extensionmethod2() {}
// Denied because its protected
protected function protectedextensionmethod() {}
public function internalextensionmethod() {}
}
class ControllerTest_HasAction extends Controller {

View File

@ -227,7 +227,7 @@ class DirectorTest extends SapphireTest {
}
public function testForceSSLOnSubPagesPattern() {
$_SERVER['REQUEST_URI'] = Director::baseURL() . 'Security/login';
$_SERVER['REQUEST_URI'] = Director::baseURL() . Config::inst()->get('Security', 'login_url');
$output = Director::forceSSL(array('/^Security/'));
$this->assertEquals($output, 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
}

View File

@ -15,7 +15,6 @@ class HTTPResponseTest extends SapphireTest {
public function testContentLengthHeader() {
$r = new SS_HTTPResponse('123ü');
$r->fixContentLength();
$this->assertNotNull($r->getHeader('Content-Length'), 'Content-length header is added');
$this->assertEquals(
5,
@ -24,7 +23,6 @@ class HTTPResponseTest extends SapphireTest {
);
$r->setBody('1234ü');
$r->fixContentLength();
$this->assertEquals(
6,
$r->getHeader('Content-Length'),

View File

@ -120,4 +120,82 @@ class HTTPTest extends SapphireTest {
HTTP::get_mime_type(FRAMEWORK_DIR.'/tests/control/files/file.psd'));
$this->assertEquals('audio/x-wav', HTTP::get_mime_type(FRAMEWORK_DIR.'/tests/control/files/file.wav'));
}
/**
* Test that absoluteURLs correctly transforms urls within CSS to absolute
*/
public function testAbsoluteURLsCSS() {
$this->withBaseURL('http://www.silverstripe.org/', function($test){
// background-image
// Note that using /./ in urls is absolutely acceptable
$test->assertEquals(
'<div style="background-image: url(\'http://www.silverstripe.org/./images/mybackground.gif\');">Content</div>',
HTTP::absoluteURLs('<div style="background-image: url(\'./images/mybackground.gif\');">Content</div>')
);
// background
$test->assertEquals(
'<div style="background: url(\'http://www.silverstripe.org/images/mybackground.gif\');">Content</div>',
HTTP::absoluteURLs('<div style="background: url(\'images/mybackground.gif\');">Content</div>')
);
// list-style-image
$test->assertEquals(
'<div style=\'background: url(http://www.silverstripe.org/list.png);\'>Content</div>',
HTTP::absoluteURLs('<div style=\'background: url(list.png);\'>Content</div>')
);
// list-style
$test->assertEquals(
'<div style=\'background: url("http://www.silverstripe.org/./assets/list.png");\'>Content</div>',
HTTP::absoluteURLs('<div style=\'background: url("./assets/list.png");\'>Content</div>')
);
});
}
/**
* Test that absoluteURLs correctly transforms urls within html attributes to absolute
*/
public function testAbsoluteURLsAttributes() {
$this->withBaseURL('http://www.silverstripe.org/', function($test){
// links
$test->assertEquals(
'<a href=\'http://www.silverstripe.org/blog/\'>SS Blog</a>',
HTTP::absoluteURLs('<a href=\'/blog/\'>SS Blog</a>')
);
// background
// Note that using /./ in urls is absolutely acceptable
$test->assertEquals(
'<div background="http://www.silverstripe.org/./themes/silverstripe/images/nav-bg-repeat-2.png">SS Blog</div>',
HTTP::absoluteURLs('<div background="./themes/silverstripe/images/nav-bg-repeat-2.png">SS Blog</div>')
);
// image
$test->assertEquals(
'<img src=\'http://www.silverstripe.org/themes/silverstripe/images/logo-org.png\' />',
HTTP::absoluteURLs('<img src=\'themes/silverstripe/images/logo-org.png\' />')
);
// link
$test->assertEquals(
'<link href=http://www.silverstripe.org/base.css />',
HTTP::absoluteURLs('<link href=base.css />')
);
});
}
/**
* Run a test while mocking the base url with the provided value
* @param string $url The base URL to use for this test
* @param callable $callback The test to run
*/
protected function withBaseURL($url, $callback) {
$oldBase = Director::$alternateBaseURL;
Director::setBaseURL($url);
$callback($this);
Director::setBaseURL($oldBase);
}
}

View File

@ -65,17 +65,13 @@ class RequestHandlingTest extends FunctionalTest {
}
public function testBadBase() {
/* Without a double-slash indicator in the URL, the entire URL is popped off the stack. The controller's
* default action handlers have been designed for this to an extend: simple actions can still be called.
* This is the set-up of URL rules written before this new request handler. */
/* We no longer support using hacky attempting to handle URL parsing with broken rules */
$response = Director::test("testBadBase/method/1/2");
$this->assertEquals("This is a method on the controller: 1, 2", $response->getBody());
$this->assertNotEquals("This is a method on the controller: 1, 2", $response->getBody());
$response = Director::test("testBadBase/TestForm", array("MyField" => 3), null, "POST");
$this->assertEquals("Form posted", $response->getBody());
$this->assertNotEquals("Form posted", $response->getBody());
/* It won't, however, let you chain requests to access methods on forms, or form fields. In order to do that,
* you need to have a // marker in your URL parsing rule */
$response = Director::test("testBadBase/TestForm/fields/MyField");
$this->assertNotEquals("MyField requested", $response->getBody());
}
@ -162,7 +158,7 @@ class RequestHandlingTest extends FunctionalTest {
public function testMethodsOnParentClassesOfRequestHandlerDeclined() {
$response = Director::test('testGoodBase1/getIterator');
$this->assertEquals(403, $response->getStatusCode());
$this->assertEquals(404, $response->getStatusCode());
}
public function testFormActionsCanBypassAllowedActions() {
@ -295,6 +291,17 @@ Config::inst()->update('Director', 'rules', array(
* Controller for the test
*/
class RequestHandlingTest_Controller extends Controller implements TestOnly {
static $allowed_actions = array(
'method',
'legacymethod',
'virtualfile',
'TestForm',
'throwexception',
'throwresponseexception',
'throwhttperror',
);
static $url_handlers = array(
// The double-slash is need here to ensure that
'$Action//$ID/$OtherID' => "handleAction",

View File

@ -65,6 +65,49 @@ class SS_LogTest extends SapphireTest {
);
}
protected function exceptionGeneratorThrower() {
throw new Exception("thrown from SS_LogTest::testExceptionGeneratorTop");
}
protected function exceptionGenerator() {
$this->exceptionGeneratorThrower();
}
public function testEmailException() {
$testEmailWriter = new SS_LogEmailWriter('test@test.com');
SS_Log::add_writer($testEmailWriter, SS_Log::ERR);
// Trigger exception handling mechanism
try {
$this->exceptionGenerator();
} catch(Exception $exception) {
// Mimics exceptionHandler, but without the exit(1)
SS_Log::log(
array(
'errno' => E_USER_ERROR,
'errstr' => ("Uncaught " . get_class($exception) . ": " . $exception->getMessage()),
'errfile' => $exception->getFile(),
'errline' => $exception->getLine(),
'errcontext' => $exception->getTrace()
),
SS_Log::ERR
);
}
// Ensure email is sent
$this->assertEmailSent('test@test.com');
// Begin parsing of email body
$email = $this->findEmail('test@test.com');
$parser = new CSSContentParser($email['htmlContent']);
// Check that the first three lines of the stacktrace are correct
$stacktrace = $parser->getByXpath('//body/div[1]/ul[1]');
$this->assertContains('<b>SS_LogTest-&gt;exceptionGeneratorThrower()</b>', $stacktrace[0]->li[0]->asXML());
$this->assertContains('<b>SS_LogTest-&gt;exceptionGenerator()</b>', $stacktrace[0]->li[1]->asXML());
$this->assertContains('<b>SS_LogTest-&gt;testEmailException()</b>', $stacktrace[0]->li[2]->asXML());
}
public function testSubclassedLogger() {
$this->assertTrue(SS_Log::get_logger() !== SS_LogTest_NewLogger::get_logger());
}

View File

@ -25,16 +25,9 @@ class DatetimeFieldTest extends SapphireTest {
}
public function testFormSaveInto() {
$form = new Form(
new Controller(),
'Form',
new FieldList(
$f = new DatetimeField('MyDatetime', null)
),
new FieldList(
new FormAction('doSubmit')
)
);
$f = new DatetimeField('MyDatetime', null);
$form = $this->getMockForm();
$form->Fields()->push($f);
$f->setValue(array(
'date' => '29/03/2003',
'time' => '23:59:38'
@ -170,6 +163,65 @@ class DatetimeFieldTest extends SapphireTest {
date_default_timezone_set($oldTz);
}
public function testSetDateField() {
$form = $this->getMockForm();
$field = new DatetimeField('Datetime', 'Datetime');
$field->setForm($form);
$field->setValue(array(
'date' => '24/06/2003',
'time' => '23:59:59',
));
$dateField = new DateField('Datetime[date]');
$field->setDateField($dateField);
$this->assertEquals(
$dateField->getForm(),
$form,
'Sets form on new field'
);
$this->assertEquals(
'2003-06-24',
$dateField->dataValue(),
'Sets existing value on new field'
);
}
public function testSetTimeField() {
$form = $this->getMockForm();
$field = new DatetimeField('Datetime', 'Datetime');
$field->setForm($form);
$field->setValue(array(
'date' => '24/06/2003',
'time' => '23:59:59',
));
$timeField = new TimeField('Datetime[time]');
$field->setTimeField($timeField);
$this->assertEquals(
$timeField->getForm(),
$form,
'Sets form on new field'
);
$this->assertEquals(
'23:59:59',
$timeField->dataValue(),
'Sets existing value on new field'
);
}
protected function getMockForm() {
return new Form(
new Controller(),
'Form',
new FieldList(),
new FieldList(
new FormAction('doSubmit')
)
);
}
}
/**

View File

@ -239,7 +239,7 @@ class GridFieldTest extends SapphireTest {
public function testHandleActionBadArgument() {
$this->setExpectedException('InvalidArgumentException');
$obj = new GridField('testfield', 'testfield');
$obj->handleAction('prft', array(), array());
$obj->handleAlterAction('prft', array(), array());
}
/**
@ -248,7 +248,7 @@ class GridFieldTest extends SapphireTest {
public function testHandleAction() {
$config = GridFieldConfig::create()->addComponent(new GridFieldTest_Component);
$obj = new GridField('testfield', 'testfield', ArrayList::create(), $config);
$this->assertEquals('handledAction is executed', $obj->handleAction('jump', array(), array()));
$this->assertEquals('handledAction is executed', $obj->handleAlterAction('jump', array(), array()));
}
/**

View File

@ -167,6 +167,29 @@ class RequirementsTest extends SapphireTest {
$backend->delete_combined_files('RequirementsTest_bc.js');
}
public function testCombinedCss() {
$basePath = $this->getCurrentRelativePath();
$backend = new Requirements_Backend;
$backend->set_combined_files_enabled(true);
$backend->combine_files(
'print.css',
array(
$basePath . '/RequirementsTest_print_a.css',
$basePath . '/RequirementsTest_print_b.css'
),
'print'
);
$html = $backend->includeInHTML(false, self::$html_template);
$this->assertTrue((bool)preg_match('/href=".*\/print\.css/', $html), 'Print stylesheets have been combined.');
$this->assertTrue((bool)preg_match(
'/media="print/', $html),
'Combined print stylesheet retains the media parameter'
);
}
public function testBlockedCombinedJavascript() {
$basePath = $this->getCurrentRelativePath();

View File

View File

View File

@ -523,6 +523,24 @@ class InjectorTest extends SapphireTest {
$this->assertInstanceOf('OtherTestObject', $item->property->property);
}
public function testNamedServices() {
$injector = new Injector();
$service = new stdClass();
$injector->registerNamedService('NamedService', $service);
$this->assertEquals($service, $injector->get('NamedService'));
}
public function testCreateConfiggedObjectWithCustomConstructorArgs() {
// need to make sure that even if the config defines some constructor params,
// that we take our passed in constructor args instead
$injector = new Injector(array('locator' => 'InjectorTestConfigLocator'));
$item = $injector->create('ConfigConstructor', 'othervalue');
$this->assertEquals($item->property, 'othervalue');
}
}
class InjectorTestConfigLocator extends SilverStripeServiceConfigurationLocator implements TestOnly {
@ -531,6 +549,10 @@ class InjectorTestConfigLocator extends SilverStripeServiceConfigurationLocator
return array('class' => 'ConstructableObject', 'constructor' => array('%$OtherTestObject'));
}
if ($name == 'ConfigConstructor') {
return array('class' => 'ConstructableObject', 'constructor' => array('value'));
}
return parent::locateConfigFor($name);
}
}

View File

@ -268,6 +268,7 @@ class DataObjectLazyLoadingTest extends SapphireTest {
$obj1->write();
$version2 = $obj1->Version;
$reloaded = Versioned::get_version('VersionedTest_Subclass', $obj1->ID, $version1);
$this->assertEquals($reloaded->Name, 'test');
$this->assertEquals($reloaded->ExtraField, 'foo');
@ -275,6 +276,138 @@ class DataObjectLazyLoadingTest extends SapphireTest {
$reloaded = Versioned::get_version('VersionedTest_Subclass', $obj1->ID, $version2);
$this->assertEquals($reloaded->Name, 'test2');
$this->assertEquals($reloaded->ExtraField, 'baz');
$reloaded = Versioned::get_latest_version('VersionedTest_Subclass', $obj1->ID);
$this->assertEquals($reloaded->Version, $version2);
$this->assertEquals($reloaded->Name, 'test2');
$this->assertEquals($reloaded->ExtraField, 'baz');
$allVersions = Versioned::get_all_versions('VersionedTest_Subclass', $obj1->ID);
$this->assertEquals(2, $allVersions->Count());
$this->assertEquals($allVersions->First()->Version, $version1);
$this->assertEquals($allVersions->First()->Name, 'test');
$this->assertEquals($allVersions->First()->ExtraField, 'foo');
$this->assertEquals($allVersions->Last()->Version, $version2);
$this->assertEquals($allVersions->Last()->Name, 'test2');
$this->assertEquals($allVersions->Last()->ExtraField, 'baz');
$obj1->delete();
}
public function testLazyLoadedFieldsDoNotReferenceVersionsTable() {
// Save another record, sanity check that we're getting the right one
$obj2 = new VersionedTest_Subclass();
$obj2->Name = "test2";
$obj2->ExtraField = "foo2";
$obj2->write();
$obj1 = new VersionedLazySub_DataObject();
$obj1->PageName = "old-value";
$obj1->ExtraField = "old-value";
$obj1ID = $obj1->write();
$obj1->publish('Stage', 'Live');
$obj1 = VersionedLazySub_DataObject::get()->byID($obj1ID);
$this->assertEquals(
'old-value',
$obj1->PageName,
"Correct value on base table when fetching base class"
);
$this->assertEquals(
'old-value',
$obj1->ExtraField,
"Correct value on sub table when fetching base class"
);
$obj1 = VersionedLazy_DataObject::get()->byID($obj1ID);
$this->assertEquals(
'old-value',
$obj1->PageName,
"Correct value on base table when fetching sub class"
);
$this->assertEquals(
'old-value',
$obj1->ExtraField,
"Correct value on sub table when fetching sub class"
);
// Force inconsistent state to test behaviour (shouldn't select from *_versions)
DB::query(sprintf(
"UPDATE \"VersionedLazy_DataObject_versions\" SET \"PageName\" = 'versioned-value' " .
"WHERE \"RecordID\" = %d",
$obj1ID
));
DB::query(sprintf(
"UPDATE \"VersionedLazySub_DataObject_versions\" SET \"ExtraField\" = 'versioned-value' " .
"WHERE \"RecordID\" = %d",
$obj1ID
));
$obj1 = VersionedLazySub_DataObject::get()->byID($obj1ID);
$this->assertEquals(
'old-value',
$obj1->PageName,
"Correct value on base table when fetching base class"
);
$this->assertEquals(
'old-value',
$obj1->ExtraField,
"Correct value on sub table when fetching base class"
);
$obj1 = VersionedLazy_DataObject::get()->byID($obj1ID);
$this->assertEquals(
'old-value',
$obj1->PageName,
"Correct value on base table when fetching sub class"
);
$this->assertEquals(
'old-value',
$obj1->ExtraField,
"Correct value on sub table when fetching sub class"
);
// Update live table only to test behaviour (shouldn't select from *_versions or stage)
DB::query(sprintf(
'UPDATE "VersionedLazy_DataObject_Live" SET "PageName" = \'live-value\' WHERE "ID" = %d',
$obj1ID
));
DB::query(sprintf(
'UPDATE "VersionedLazySub_DataObject_Live" SET "ExtraField" = \'live-value\' WHERE "ID" = %d',
$obj1ID
));
Versioned::reading_stage('Live');
$obj1 = VersionedLazy_DataObject::get()->byID($obj1ID);
$this->assertEquals(
'live-value',
$obj1->PageName,
"Correct value from base table when fetching base class on live stage"
);
$this->assertEquals(
'live-value',
$obj1->ExtraField,
"Correct value from sub table when fetching base class on live stage"
);
}
}
/** Additional classes for versioned lazy loading testing */
class VersionedLazy_DataObject extends DataObject {
static $db = array(
"PageName" => "Varchar"
);
static $extensions = array(
"Versioned('Stage', 'Live')"
);
}
class VersionedLazySub_DataObject extends VersionedLazy_DataObject {
static $db = array(
"ExtraField" => "Varchar",
);
static $extensions = array(
"Versioned('Stage', 'Live')"
);
}

View File

@ -139,4 +139,38 @@ class HTMLTextTest extends SapphireTest {
$data = DBField::create_field('HTMLText', '"this is a test"');
$this->assertEquals($data->ATT(), '&quot;this is a test&quot;');
}
function testExists() {
$h = new HTMLText;
$h->setValue("");
$this->assertFalse($h->exists());
$h->setValue("<p></p>");
$this->assertFalse($h->exists());
$h->setValue("<p> </p>");
$this->assertFalse($h->exists());
$h->setValue("<h2/>");
$this->assertFalse($h->exists());
$h->setValue("<h2></h2>");
$this->assertFalse($h->exists());
$h->setValue("something");
$this->assertTrue($h->exists());
$h->setValue("<img src=\"dummy.png\">");
$this->assertTrue($h->exists());
$h->setValue("<img src=\"dummy.png\"><img src=\"dummy.png\">");
$this->assertTrue($h->exists());
$h->setValue("<p><img src=\"dummy.png\"></p>");
$this->assertTrue($h->exists());
$h->setValue("<iframe src=\"http://www.google.com\"></iframe>");
$this->assertTrue($h->exists());
$h->setValue("<embed src=\"test.swf\">");
$this->assertTrue($h->exists());
$h->setValue("<object width=\"400\" height=\"400\" data=\"test.swf\"></object>");
$this->assertTrue($h->exists());
$h->setValue("<p>test</p>");
$this->assertTrue($h->exists());
}
}

View File

@ -18,6 +18,19 @@ class ShortcodeParserTest extends SapphireTest {
* Tests that valid short codes that have not been registered are not replaced.
*/
public function testNotRegisteredShortcode() {
ShortcodeParser::$error_behavior = ShortcodeParser::STRIP;
$this->assertEquals(
'',
$this->parser->parse('[not_shortcode]')
);
ShortcodeParser::$error_behavior = ShortcodeParser::WARN;
$this->assertEquals(
'<strong class="warning">[not_shortcode]</strong>',
$this->parser->parse('[not_shortcode]')
);
ShortcodeParser::$error_behavior = ShortcodeParser::LEAVE;
$this->assertEquals('[not_shortcode]',
$this->parser->parse('[not_shortcode]'));
$this->assertEquals('[not_shortcode /]',
@ -26,11 +39,16 @@ class ShortcodeParserTest extends SapphireTest {
$this->parser->parse('[not_shortcode,foo="bar"]'));
$this->assertEquals('[not_shortcode]a[/not_shortcode]',
$this->parser->parse('[not_shortcode]a[/not_shortcode]'));
$this->assertEquals('[/not_shortcode]',
$this->parser->parse('[/not_shortcode]'));
}
public function testSimpleTag() {
$tests = array('[test_shortcode]', '[test_shortcode ]', '[test_shortcode,]', '[test_shortcode/]',
'[test_shortcode /]');
$tests = array(
'[test_shortcode]',
'[test_shortcode ]', '[test_shortcode,]', '[test_shortcode, ]'.
'[test_shortcode/]', '[test_shortcode /]', '[test_shortcode,/]', '[test_shortcode, /]'
);
foreach($tests as $test) {
$this->parser->parse($test);
@ -43,9 +61,9 @@ class ShortcodeParserTest extends SapphireTest {
public function testOneArgument() {
$tests = array (
'[test_shortcode,foo="bar"]',
"[test_shortcode,foo='bar']",
'[test_shortcode,foo = "bar" /]'
'[test_shortcode foo="bar"]', '[test_shortcode,foo="bar"]',
"[test_shortcode foo='bar']", "[test_shortcode,foo='bar']",
'[test_shortcode foo = "bar" /]', '[test_shortcode, foo = "bar" /]'
);
foreach($tests as $test) {
@ -58,7 +76,7 @@ class ShortcodeParserTest extends SapphireTest {
}
public function testMultipleArguments() {
$this->parser->parse('[test_shortcode,foo = "bar",bar=\'foo\',baz="buz"]');
$this->parser->parse('[test_shortcode foo = "bar",bar=\'foo\', baz="buz"]');
$this->assertEquals(array('foo' => 'bar', 'bar' => 'foo', 'baz' => 'buz'), $this->arguments);
$this->assertEquals('', $this->contents);
@ -86,7 +104,7 @@ class ShortcodeParserTest extends SapphireTest {
$this->assertEquals('[test_shortcode]content[/test_shortcode]',
$this->parser->parse('[[test_shortcode]content[/test_shortcode]]'));
}
public function testUnquotedArguments() {
$this->assertEquals('', $this->parser->parse('[test_shortcode,foo=bar,baz = buz]'));
$this->assertEquals(array('foo' => 'bar', 'baz' => 'buz'), $this->arguments);
@ -111,6 +129,42 @@ class ShortcodeParserTest extends SapphireTest {
$this->assertEquals('', $this->parser->parse('[test_shortcode][test_shortcode]'));
}
protected function assertEqualsIgnoringWhitespace($a, $b, $message = null) {
$this->assertEquals(preg_replace('/\s+/', '', $a), preg_replace('/\s+/', '', $b), $message);
}
public function testtExtract() {
// Left extracts to before the current block
$this->assertEqualsIgnoringWhitespace(
'Code<div>FooBar</div>',
$this->parser->parse('<div>Foo[test_shortcode class=left]Code[/test_shortcode]Bar</div>')
);
// Even if the immediate parent isn't a the current block
$this->assertEqualsIgnoringWhitespace(
'Code<div>Foo<b>BarBaz</b>Qux</div>',
$this->parser->parse('<div>Foo<b>Bar[test_shortcode class=left]Code[/test_shortcode]Baz</b>Qux</div>')
);
// Center splits the current block
$this->assertEqualsIgnoringWhitespace(
'<div>Foo</div>Code<div>Bar</div>',
$this->parser->parse('<div>Foo[test_shortcode class=center]Code[/test_shortcode]Bar</div>')
);
// Even if the immediate parent isn't a the current block
$this->assertEqualsIgnoringWhitespace(
'<div>Foo<b>Bar</b></div>Code<div><b>Baz</b>Qux</div>',
$this->parser->parse('<div>Foo<b>Bar[test_shortcode class=center]Code[/test_shortcode]Baz</b>Qux</div>')
);
// No class means don't extract
$this->assertEqualsIgnoringWhitespace(
'<div>FooCodeBar</div>',
$this->parser->parse('<div>Foo[test_shortcode]Code[/test_shortcode]Bar</div>')
);
}
// -----------------------------------------------------------------------------------------------------------------
/**

View File

@ -114,6 +114,23 @@ class MemberTest extends FunctionalTest {
Security::set_password_encryption_algorithm($origAlgo);
}
public function testKeepsEncryptionOnEmptyPasswords() {
$member = new Member();
$member->Password = 'mypassword';
$member->PasswordEncryption = 'sha1_v2.4';
$member->write();
$member->Password = '';
$member->write();
$this->assertEquals(
$member->PasswordEncryption,
'sha1_v2.4'
);
$result = $member->checkPassword('');
$this->assertTrue($result->valid());
}
public function testSetPassword() {
$member = $this->objFromFixture('Member', 'test');

View File

@ -57,7 +57,10 @@ class SecurityTest extends FunctionalTest {
$response = $this->get('SecurityTest_SecuredController');
$this->assertEquals(302, $response->getStatusCode());
$this->assertContains('Security/login', $response->getHeader('Location'));
$this->assertContains(
Config::inst()->get('Security', 'login_url'),
$response->getHeader('Location')
);
$this->logInWithPermission('ADMIN');
$response = $this->get('SecurityTest_SecuredController');
@ -74,7 +77,7 @@ class SecurityTest extends FunctionalTest {
$this->session()->inst_set('loggedInAs', $member->ID);
/* View the Security/login page */
$response = $this->get('Security/login');
$response = $this->get(Config::inst()->get('Security', 'login_url'));
$items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.action');
@ -108,7 +111,7 @@ class SecurityTest extends FunctionalTest {
$this->autoFollowRedirection = true;
/* Attempt to get into the admin section */
$response = $this->get('Security/login/');
$response = $this->get(Config::inst()->get('Security', 'login_url'));
$items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.text');
@ -396,7 +399,7 @@ class SecurityTest extends FunctionalTest {
public function doTestLoginForm($email, $password, $backURL = 'test/link') {
$this->get('Security/logout');
$this->session()->inst_set('BackURL', $backURL);
$this->get('Security/login');
$this->get(Config::inst()->get('Security', 'login_url'));
return $this->submitForm(
"MemberLoginForm_LoginForm",

View File

@ -0,0 +1 @@
$A.Key $B.Key

View File

@ -15,8 +15,8 @@ echo ""
# Fetch all dependencies
# TODO Replace with different composer.json variations
echo "Checking out installer@$TRAVIS_BRANCH"
git clone --depth=100 --quiet -b $TRAVIS_BRANCH git://github.com/silverstripe/silverstripe-installer.git $BUILD_DIR
echo "Checking out installer@3.1"
git clone --depth=100 --quiet -b 3.1 git://github.com/silverstripe/silverstripe-installer.git $BUILD_DIR
echo "Checking out sqlite3@master"
git clone --depth=100 --quiet git://github.com/silverstripe-labs/silverstripe-sqlite3.git $BUILD_DIR/sqlite3

View File

@ -508,6 +508,17 @@ after')
new ArrayData(array('Arg1' => 'Foo', 'Arg2' => 'Bar'))),
'<p>A</p><p>Bar</p>'
);
$data = new ArrayData(array(
'Nested' => new ArrayData(array(
'Object' => new ArrayData(array('Key' => 'A'))
)),
'Object' => new ArrayData(array('Key' => 'B'))
));
$tmpl = SSViewer::fromString('<% include SSViewerTestIncludeObjectArguments A=$Nested.Object, B=$Object %>');
$res = $tmpl->process($data);
$this->assertEqualIgnoringWhitespace('A B', $res, 'Objects can be passed as named arguments');
}

114
thirdparty/html5lib/HTML5/Data.php vendored Normal file
View File

@ -0,0 +1,114 @@
<?php
// warning: this file is encoded in UTF-8!
class HTML5_Data
{
// at some point this should be moved to a .ser file. Another
// possible optimization is to give UTF-8 bytes, not Unicode
// codepoints
// XXX: Not quite sure why it's named this; this is
// actually the numeric entity dereference table.
protected static $realCodepointTable = array(
0x00 => 0xFFFD, // REPLACEMENT CHARACTER
0x0D => 0x000A, // LINE FEED (LF)
0x80 => 0x20AC, // EURO SIGN ('€')
0x81 => 0x0081, // <control>
0x82 => 0x201A, // SINGLE LOW-9 QUOTATION MARK ('')
0x83 => 0x0192, // LATIN SMALL LETTER F WITH HOOK ('ƒ')
0x84 => 0x201E, // DOUBLE LOW-9 QUOTATION MARK ('„')
0x85 => 0x2026, // HORIZONTAL ELLIPSIS ('…')
0x86 => 0x2020, // DAGGER ('†')
0x87 => 0x2021, // DOUBLE DAGGER ('‡')
0x88 => 0x02C6, // MODIFIER LETTER CIRCUMFLEX ACCENT ('ˆ')
0x89 => 0x2030, // PER MILLE SIGN ('‰')
0x8A => 0x0160, // LATIN CAPITAL LETTER S WITH CARON ('Š')
0x8B => 0x2039, // SINGLE LEFT-POINTING ANGLE QUOTATION MARK ('')
0x8C => 0x0152, // LATIN CAPITAL LIGATURE OE ('Œ')
0x8D => 0x008D, // <control>
0x8E => 0x017D, // LATIN CAPITAL LETTER Z WITH CARON ('Ž')
0x8F => 0x008F, // <control>
0x90 => 0x0090, // <control>
0x91 => 0x2018, // LEFT SINGLE QUOTATION MARK ('')
0x92 => 0x2019, // RIGHT SINGLE QUOTATION MARK ('')
0x93 => 0x201C, // LEFT DOUBLE QUOTATION MARK ('“')
0x94 => 0x201D, // RIGHT DOUBLE QUOTATION MARK ('”')
0x95 => 0x2022, // BULLET ('•')
0x96 => 0x2013, // EN DASH ('')
0x97 => 0x2014, // EM DASH ('—')
0x98 => 0x02DC, // SMALL TILDE ('˜')
0x99 => 0x2122, // TRADE MARK SIGN ('™')
0x9A => 0x0161, // LATIN SMALL LETTER S WITH CARON ('š')
0x9B => 0x203A, // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK ('')
0x9C => 0x0153, // LATIN SMALL LIGATURE OE ('œ')
0x9D => 0x009D, // <control>
0x9E => 0x017E, // LATIN SMALL LETTER Z WITH CARON ('ž')
0x9F => 0x0178, // LATIN CAPITAL LETTER Y WITH DIAERESIS ('Ÿ')
);
protected static $namedCharacterReferences;
protected static $namedCharacterReferenceMaxLength;
/**
* Returns the "real" Unicode codepoint of a malformed character
* reference.
*/
public static function getRealCodepoint($ref) {
if (!isset(self::$realCodepointTable[$ref])) return false;
else return self::$realCodepointTable[$ref];
}
public static function getNamedCharacterReferences() {
if (!self::$namedCharacterReferences) {
self::$namedCharacterReferences = unserialize(
file_get_contents(dirname(__FILE__) . '/named-character-references.ser'));
}
return self::$namedCharacterReferences;
}
/**
* Converts a Unicode codepoint to sequence of UTF-8 bytes.
* @note Shamelessly stolen from HTML Purifier, which is also
* shamelessly stolen from Feyd (which is in public domain).
*/
public static function utf8chr($code) {
/* We don't care: we live dangerously
* if($code > 0x10FFFF or $code < 0x0 or
($code >= 0xD800 and $code <= 0xDFFF) ) {
// bits are set outside the "valid" range as defined
// by UNICODE 4.1.0
return "\xEF\xBF\xBD";
}*/
$x = $y = $z = $w = 0;
if ($code < 0x80) {
// regular ASCII character
$x = $code;
} else {
// set up bits for UTF-8
$x = ($code & 0x3F) | 0x80;
if ($code < 0x800) {
$y = (($code & 0x7FF) >> 6) | 0xC0;
} else {
$y = (($code & 0xFC0) >> 6) | 0x80;
if($code < 0x10000) {
$z = (($code >> 12) & 0x0F) | 0xE0;
} else {
$z = (($code >> 12) & 0x3F) | 0x80;
$w = (($code >> 18) & 0x07) | 0xF0;
}
}
}
// set up the actual character
$ret = '';
if($w) $ret .= chr($w);
if($z) $ret .= chr($z);
if($y) $ret .= chr($y);
$ret .= chr($x);
return $ret;
}
}

View File

@ -0,0 +1,284 @@
<?php
/*
Copyright 2009 Geoffrey Sneddon <http://gsnedders.com/>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// Some conventions:
// /* */ indicates verbatim text from the HTML 5 specification
// // indicates regular comments
class HTML5_InputStream {
/**
* The string data we're parsing.
*/
private $data;
/**
* The current integer byte position we are in $data
*/
private $char;
/**
* Length of $data; when $char === $data, we are at the end-of-file.
*/
private $EOF;
/**
* Parse errors.
*/
public $errors = array();
/**
* @param $data Data to parse
*/
public function __construct($data) {
/* Given an encoding, the bytes in the input stream must be
converted to Unicode characters for the tokeniser, as
described by the rules for that encoding, except that the
leading U+FEFF BYTE ORDER MARK character, if any, must not
be stripped by the encoding layer (it is stripped by the rule below).
Bytes or sequences of bytes in the original byte stream that
could not be converted to Unicode characters must be converted
to U+FFFD REPLACEMENT CHARACTER code points. */
// XXX currently assuming input data is UTF-8; once we
// build encoding detection this will no longer be the case
//
// We previously had an mbstring implementation here, but that
// implementation is heavily non-conforming, so it's been
// omitted.
if (extension_loaded('iconv')) {
// non-conforming
$data = @iconv('UTF-8', 'UTF-8//IGNORE', $data);
} else {
// we can make a conforming native implementation
throw new Exception('Not implemented, please install mbstring or iconv');
}
/* One leading U+FEFF BYTE ORDER MARK character must be
ignored if any are present. */
if (substr($data, 0, 3) === "\xEF\xBB\xBF") {
$data = substr($data, 3);
}
/* All U+0000 NULL characters in the input must be replaced
by U+FFFD REPLACEMENT CHARACTERs. Any occurrences of such
characters is a parse error. */
for ($i = 0, $count = substr_count($data, "\0"); $i < $count; $i++) {
$this->errors[] = array(
'type' => HTML5_Tokenizer::PARSEERROR,
'data' => 'null-character'
);
}
/* U+000D CARRIAGE RETURN (CR) characters and U+000A LINE FEED
(LF) characters are treated specially. Any CR characters
that are followed by LF characters must be removed, and any
CR characters not followed by LF characters must be converted
to LF characters. Thus, newlines in HTML DOMs are represented
by LF characters, and there are never any CR characters in the
input to the tokenization stage. */
$data = str_replace(
array(
"\0",
"\r\n",
"\r"
),
array(
"\xEF\xBF\xBD",
"\n",
"\n"
),
$data
);
/* Any occurrences of any characters in the ranges U+0001 to
U+0008, U+000B, U+000E to U+001F, U+007F to U+009F,
U+D800 to U+DFFF , U+FDD0 to U+FDEF, and
characters U+FFFE, U+FFFF, U+1FFFE, U+1FFFF, U+2FFFE, U+2FFFF,
U+3FFFE, U+3FFFF, U+4FFFE, U+4FFFF, U+5FFFE, U+5FFFF, U+6FFFE,
U+6FFFF, U+7FFFE, U+7FFFF, U+8FFFE, U+8FFFF, U+9FFFE, U+9FFFF,
U+AFFFE, U+AFFFF, U+BFFFE, U+BFFFF, U+CFFFE, U+CFFFF, U+DFFFE,
U+DFFFF, U+EFFFE, U+EFFFF, U+FFFFE, U+FFFFF, U+10FFFE, and
U+10FFFF are parse errors. (These are all control characters
or permanently undefined Unicode characters.) */
// Check PCRE is loaded.
if (extension_loaded('pcre')) {
$count = preg_match_all(
'/(?:
[\x01-\x08\x0B\x0E-\x1F\x7F] # U+0001 to U+0008, U+000B, U+000E to U+001F and U+007F
|
\xC2[\x80-\x9F] # U+0080 to U+009F
|
\xED(?:\xA0[\x80-\xFF]|[\xA1-\xBE][\x00-\xFF]|\xBF[\x00-\xBF]) # U+D800 to U+DFFFF
|
\xEF\xB7[\x90-\xAF] # U+FDD0 to U+FDEF
|
\xEF\xBF[\xBE\xBF] # U+FFFE and U+FFFF
|
[\xF0-\xF4][\x8F-\xBF]\xBF[\xBE\xBF] # U+nFFFE and U+nFFFF (1 <= n <= 10_{16})
)/x',
$data,
$matches
);
for ($i = 0; $i < $count; $i++) {
$this->errors[] = array(
'type' => HTML5_Tokenizer::PARSEERROR,
'data' => 'invalid-codepoint'
);
}
} else {
// XXX: Need non-PCRE impl, probably using substr_count
}
$this->data = $data;
$this->char = 0;
$this->EOF = strlen($data);
}
/**
* Returns the current line that the tokenizer is at.
*/
public function getCurrentLine() {
// Check the string isn't empty
if($this->EOF) {
// Add one to $this->char because we want the number for the next
// byte to be processed.
return substr_count($this->data, "\n", 0, min($this->char, $this->EOF)) + 1;
} else {
// If the string is empty, we are on the first line (sorta).
return 1;
}
}
/**
* Returns the current column of the current line that the tokenizer is at.
*/
public function getColumnOffset() {
// strrpos is weird, and the offset needs to be negative for what we
// want (i.e., the last \n before $this->char). This needs to not have
// one (to make it point to the next character, the one we want the
// position of) added to it because strrpos's behaviour includes the
// final offset byte.
$lastLine = strrpos($this->data, "\n", $this->char - 1 - strlen($this->data));
// However, for here we want the length up until the next byte to be
// processed, so add one to the current byte ($this->char).
if($lastLine !== false) {
$findLengthOf = substr($this->data, $lastLine + 1, $this->char - 1 - $lastLine);
} else {
$findLengthOf = substr($this->data, 0, $this->char);
}
// Get the length for the string we need.
if(extension_loaded('iconv')) {
return iconv_strlen($findLengthOf, 'utf-8');
} elseif(extension_loaded('mbstring')) {
return mb_strlen($findLengthOf, 'utf-8');
} elseif(extension_loaded('xml')) {
return strlen(utf8_decode($findLengthOf));
} else {
$count = count_chars($findLengthOf);
// 0x80 = 0x7F - 0 + 1 (one added to get inclusive range)
// 0x33 = 0xF4 - 0x2C + 1 (one added to get inclusive range)
return array_sum(array_slice($count, 0, 0x80)) +
array_sum(array_slice($count, 0xC2, 0x33));
}
}
/**
* Retrieve the currently consume character.
* @note This performs bounds checking
*/
public function char() {
return ($this->char++ < $this->EOF)
? $this->data[$this->char - 1]
: false;
}
/**
* Get all characters until EOF.
* @note This performs bounds checking
*/
public function remainingChars() {
if($this->char < $this->EOF) {
$data = substr($this->data, $this->char);
$this->char = $this->EOF;
return $data;
} else {
return false;
}
}
/**
* Matches as far as possible until we reach a certain set of bytes
* and returns the matched substring.
* @param $bytes Bytes to match.
*/
public function charsUntil($bytes, $max = null) {
if ($this->char < $this->EOF) {
if ($max === 0 || $max) {
$len = strcspn($this->data, $bytes, $this->char, $max);
} else {
$len = strcspn($this->data, $bytes, $this->char);
}
$string = (string) substr($this->data, $this->char, $len);
$this->char += $len;
return $string;
} else {
return false;
}
}
/**
* Matches as far as possible with a certain set of bytes
* and returns the matched substring.
* @param $bytes Bytes to match.
*/
public function charsWhile($bytes, $max = null) {
if ($this->char < $this->EOF) {
if ($max === 0 || $max) {
$len = strspn($this->data, $bytes, $this->char, $max);
} else {
$len = strspn($this->data, $bytes, $this->char);
}
$string = (string) substr($this->data, $this->char, $len);
$this->char += $len;
return $string;
} else {
return false;
}
}
/**
* Unconsume one character.
*/
public function unget() {
if ($this->char <= $this->EOF) {
$this->char--;
}
}
}

36
thirdparty/html5lib/HTML5/Parser.php vendored Normal file
View File

@ -0,0 +1,36 @@
<?php
require_once dirname(__FILE__) . '/Data.php';
require_once dirname(__FILE__) . '/InputStream.php';
require_once dirname(__FILE__) . '/TreeBuilder.php';
require_once dirname(__FILE__) . '/Tokenizer.php';
/**
* Outwards facing interface for HTML5.
*/
class HTML5_Parser
{
/**
* Parses a full HTML document.
* @param $text HTML text to parse
* @param $builder Custom builder implementation
* @return Parsed HTML as DOMDocument
*/
static public function parse($text, $builder = null) {
$tokenizer = new HTML5_Tokenizer($text, $builder);
$tokenizer->parse();
return $tokenizer->save();
}
/**
* Parses an HTML fragment.
* @param $text HTML text to parse
* @param $context String name of context element to pretend parsing is in.
* @param $builder Custom builder implementation
* @return Parsed HTML as DOMDocument
*/
static public function parseFragment($text, $context = null, $builder = null) {
$tokenizer = new HTML5_Tokenizer($text, $builder);
$tokenizer->parseFragment($context);
return $tokenizer->save();
}
}

2422
thirdparty/html5lib/HTML5/Tokenizer.php vendored Normal file
View File

@ -0,0 +1,2422 @@
<?php
/*
Copyright 2007 Jeroen van der Meer <http://jero.net/>
Copyright 2008 Edward Z. Yang <http://htmlpurifier.org/>
Copyright 2009 Geoffrey Sneddon <http://gsnedders.com/>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// Some conventions:
// /* */ indicates verbatim text from the HTML 5 specification
// // indicates regular comments
// all flags are in hyphenated form
class HTML5_Tokenizer {
/**
* Points to an InputStream object.
*/
protected $stream;
/**
* Tree builder that the tokenizer emits token to.
*/
private $tree;
/**
* Current content model we are parsing as.
*/
protected $content_model;
/**
* Current token that is being built, but not yet emitted. Also
* is the last token emitted, if applicable.
*/
protected $token;
// These are constants describing the content model
const PCDATA = 0;
const RCDATA = 1;
const CDATA = 2;
const PLAINTEXT = 3;
// These are constants describing tokens
// XXX should probably be moved somewhere else, probably the
// HTML5 class.
const DOCTYPE = 0;
const STARTTAG = 1;
const ENDTAG = 2;
const COMMENT = 3;
const CHARACTER = 4;
const SPACECHARACTER = 5;
const EOF = 6;
const PARSEERROR = 7;
// These are constants representing bunches of characters.
const ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const UPPER_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const LOWER_ALPHA = 'abcdefghijklmnopqrstuvwxyz';
const DIGIT = '0123456789';
const HEX = '0123456789ABCDEFabcdef';
const WHITESPACE = "\t\n\x0c ";
/**
* @param $data Data to parse
*/
public function __construct($data, $builder = null) {
$this->stream = new HTML5_InputStream($data);
if (!$builder) $this->tree = new HTML5_TreeBuilder;
else $this->tree = $builder;
$this->content_model = self::PCDATA;
}
public function parseFragment($context = null) {
$this->tree->setupContext($context);
if ($this->tree->content_model) {
$this->content_model = $this->tree->content_model;
$this->tree->content_model = null;
}
$this->parse();
}
// XXX maybe convert this into an iterator? regardless, this function
// and the save function should go into a Parser facade of some sort
/**
* Performs the actual parsing of the document.
*/
public function parse() {
// Current state
$state = 'data';
// This is used to avoid having to have look-behind in the data state.
$lastFourChars = '';
/**
* Escape flag as specified by the HTML5 specification: "used to
* control the behavior of the tokeniser. It is either true or
* false, and initially must be set to the false state."
*/
$escape = false;
//echo "\n\n";
while($state !== null) {
/*echo $state . ' ';
switch ($this->content_model) {
case self::PCDATA: echo 'PCDATA'; break;
case self::RCDATA: echo 'RCDATA'; break;
case self::CDATA: echo 'CDATA'; break;
case self::PLAINTEXT: echo 'PLAINTEXT'; break;
}
if ($escape) echo " escape";
echo "\n";*/
switch($state) {
case 'data':
/* Consume the next input character */
$char = $this->stream->char();
$lastFourChars .= $char;
if (strlen($lastFourChars) > 4) $lastFourChars = substr($lastFourChars, -4);
// see below for meaning
$hyp_cond =
!$escape &&
(
$this->content_model === self::RCDATA ||
$this->content_model === self::CDATA
);
$amp_cond =
!$escape &&
(
$this->content_model === self::PCDATA ||
$this->content_model === self::RCDATA
);
$lt_cond =
$this->content_model === self::PCDATA ||
(
(
$this->content_model === self::RCDATA ||
$this->content_model === self::CDATA
) &&
!$escape
);
$gt_cond =
$escape &&
(
$this->content_model === self::RCDATA ||
$this->content_model === self::CDATA
);
if($char === '&' && $amp_cond) {
/* U+0026 AMPERSAND (&)
When the content model flag is set to one of the PCDATA or RCDATA
states and the escape flag is false: switch to the
character reference data state. Otherwise: treat it as per
the "anything else" entry below. */
$state = 'character reference data';
} elseif(
$char === '-' &&
$hyp_cond &&
$lastFourChars === '<!--'
) {
/*
U+002D HYPHEN-MINUS (-)
If the content model flag is set to either the RCDATA state or
the CDATA state, and the escape flag is false, and there are at
least three characters before this one in the input stream, and the
last four characters in the input stream, including this one, are
U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS,
and U+002D HYPHEN-MINUS ("<!--"), then set the escape flag to true. */
$escape = true;
/* In any case, emit the input character as a character token. Stay
in the data state. */
$this->emitToken(array(
'type' => self::CHARACTER,
'data' => '-'
));
// We do the "any case" part as part of "anything else".
/* U+003C LESS-THAN SIGN (<) */
} elseif($char === '<' && $lt_cond) {
/* When the content model flag is set to the PCDATA state: switch
to the tag open state.
When the content model flag is set to either the RCDATA state or
the CDATA state and the escape flag is false: switch to the tag
open state.
Otherwise: treat it as per the "anything else" entry below. */
$state = 'tag open';
/* U+003E GREATER-THAN SIGN (>) */
} elseif(
$char === '>' &&
$gt_cond &&
substr($lastFourChars, 1) === '-->'
) {
/* If the content model flag is set to either the RCDATA state or
the CDATA state, and the escape flag is true, and the last three
characters in the input stream including this one are U+002D
HYPHEN-MINUS, U+002D HYPHEN-MINUS, U+003E GREATER-THAN SIGN ("-->"),
set the escape flag to false. */
$escape = false;
/* In any case, emit the input character as a character token.
Stay in the data state. */
$this->emitToken(array(
'type' => self::CHARACTER,
'data' => '>'
));
// We do the "any case" part as part of "anything else".
} elseif($char === false) {
/* EOF
Emit an end-of-file token. */
$state = null;
$this->tree->emitToken(array(
'type' => self::EOF
));
} elseif($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
// Directly after emitting a token you switch back to the "data
// state". At that point spaceCharacters are important so they are
// emitted separately.
$chars = $this->stream->charsWhile(self::WHITESPACE);
$this->emitToken(array(
'type' => self::SPACECHARACTER,
'data' => $char . $chars
));
$lastFourChars .= $chars;
if (strlen($lastFourChars) > 4) $lastFourChars = substr($lastFourChars, -4);
} else {
/* Anything else
THIS IS AN OPTIMIZATION: Get as many character that
otherwise would also be treated as a character token and emit it
as a single character token. Stay in the data state. */
$mask = '';
if ($hyp_cond) $mask .= '-';
if ($amp_cond) $mask .= '&';
if ($lt_cond) $mask .= '<';
if ($gt_cond) $mask .= '>';
if ($mask === '') {
$chars = $this->stream->remainingChars();
} else {
$chars = $this->stream->charsUntil($mask);
}
$this->emitToken(array(
'type' => self::CHARACTER,
'data' => $char . $chars
));
$lastFourChars .= $chars;
if (strlen($lastFourChars) > 4) $lastFourChars = substr($lastFourChars, -4);
$state = 'data';
}
break;
case 'character reference data':
/* (This cannot happen if the content model flag
is set to the CDATA state.) */
/* Attempt to consume a character reference, with no
additional allowed character. */
$entity = $this->consumeCharacterReference();
/* If nothing is returned, emit a U+0026 AMPERSAND
character token. Otherwise, emit the character token that
was returned. */
// This is all done when consuming the character reference.
$this->emitToken(array(
'type' => self::CHARACTER,
'data' => $entity
));
/* Finally, switch to the data state. */
$state = 'data';
break;
case 'tag open':
$char = $this->stream->char();
switch($this->content_model) {
case self::RCDATA:
case self::CDATA:
/* Consume the next input character. If it is a
U+002F SOLIDUS (/) character, switch to the close
tag open state. Otherwise, emit a U+003C LESS-THAN
SIGN character token and reconsume the current input
character in the data state. */
// We consumed above.
if($char === '/') {
$state = 'close tag open';
} else {
$this->emitToken(array(
'type' => self::CHARACTER,
'data' => '<'
));
$this->stream->unget();
$state = 'data';
}
break;
case self::PCDATA:
/* If the content model flag is set to the PCDATA state
Consume the next input character: */
// We consumed above.
if($char === '!') {
/* U+0021 EXCLAMATION MARK (!)
Switch to the markup declaration open state. */
$state = 'markup declaration open';
} elseif($char === '/') {
/* U+002F SOLIDUS (/)
Switch to the close tag open state. */
$state = 'close tag open';
} elseif('A' <= $char && $char <= 'Z') {
/* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z
Create a new start tag token, set its tag name to the lowercase
version of the input character (add 0x0020 to the character's code
point), then switch to the tag name state. (Don't emit the token
yet; further details will be filled in before it is emitted.) */
$this->token = array(
'name' => strtolower($char),
'type' => self::STARTTAG,
'attr' => array()
);
$state = 'tag name';
} elseif('a' <= $char && $char <= 'z') {
/* U+0061 LATIN SMALL LETTER A through to U+007A LATIN SMALL LETTER Z
Create a new start tag token, set its tag name to the input
character, then switch to the tag name state. (Don't emit
the token yet; further details will be filled in before it
is emitted.) */
$this->token = array(
'name' => $char,
'type' => self::STARTTAG,
'attr' => array()
);
$state = 'tag name';
} elseif($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Parse error. Emit a U+003C LESS-THAN SIGN character token and a
U+003E GREATER-THAN SIGN character token. Switch to the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-tag-name-but-got-right-bracket'
));
$this->emitToken(array(
'type' => self::CHARACTER,
'data' => '<>'
));
$state = 'data';
} elseif($char === '?') {
/* U+003F QUESTION MARK (?)
Parse error. Switch to the bogus comment state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-tag-name-but-got-question-mark'
));
$this->token = array(
'data' => '?',
'type' => self::COMMENT
);
$state = 'bogus comment';
} else {
/* Anything else
Parse error. Emit a U+003C LESS-THAN SIGN character token and
reconsume the current input character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-tag-name'
));
$this->emitToken(array(
'type' => self::CHARACTER,
'data' => '<'
));
$state = 'data';
$this->stream->unget();
}
break;
}
break;
case 'close tag open':
if (
$this->content_model === self::RCDATA ||
$this->content_model === self::CDATA
) {
/* If the content model flag is set to the RCDATA or CDATA
states... */
$name = strtolower($this->stream->charsWhile(self::ALPHA));
$following = $this->stream->char();
$this->stream->unget();
if (
!$this->token ||
$this->token['name'] !== $name ||
$this->token['name'] === $name && !in_array($following, array("\x09", "\x0A", "\x0C", "\x20", "\x3E", "\x2F", false))
) {
/* if no start tag token has ever been emitted by this instance
of the tokenizer (fragment case), or, if the next few
characters do not match the tag name of the last start tag
token emitted (compared in an ASCII case-insensitive manner),
or if they do but they are not immediately followed by one of
the following characters:
* U+0009 CHARACTER TABULATION
* U+000A LINE FEED (LF)
* U+000C FORM FEED (FF)
* U+0020 SPACE
* U+003E GREATER-THAN SIGN (>)
* U+002F SOLIDUS (/)
* EOF
...then emit a U+003C LESS-THAN SIGN character token, a
U+002F SOLIDUS character token, and switch to the data
state to process the next input character. */
// XXX: Probably ought to replace in_array with $following === x ||...
// We also need to emit $name now we've consumed that, as we
// know it'll just be emitted as a character token.
$this->emitToken(array(
'type' => self::CHARACTER,
'data' => '</' . $name
));
$state = 'data';
} else {
// This matches what would happen if we actually did the
// otherwise below (but we can't because we've consumed too
// much).
// Start the end tag token with the name we already have.
$this->token = array(
'name' => $name,
'type' => self::ENDTAG
);
// Change to tag name state.
$state = 'tag name';
}
} elseif ($this->content_model === self::PCDATA) {
/* Otherwise, if the content model flag is set to the PCDATA
state [...]: */
$char = $this->stream->char();
if ('A' <= $char && $char <= 'Z') {
/* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z
Create a new end tag token, set its tag name to the lowercase version
of the input character (add 0x0020 to the character's code point), then
switch to the tag name state. (Don't emit the token yet; further details
will be filled in before it is emitted.) */
$this->token = array(
'name' => strtolower($char),
'type' => self::ENDTAG
);
$state = 'tag name';
} elseif ('a' <= $char && $char <= 'z') {
/* U+0061 LATIN SMALL LETTER A through to U+007A LATIN SMALL LETTER Z
Create a new end tag token, set its tag name to the
input character, then switch to the tag name state.
(Don't emit the token yet; further details will be
filled in before it is emitted.) */
$this->token = array(
'name' => $char,
'type' => self::ENDTAG
);
$state = 'tag name';
} elseif($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Parse error. Switch to the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-closing-tag-but-got-right-bracket'
));
$state = 'data';
} elseif($char === false) {
/* EOF
Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F
SOLIDUS character token. Reconsume the EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-closing-tag-but-got-eof'
));
$this->emitToken(array(
'type' => self::CHARACTER,
'data' => '</'
));
$this->stream->unget();
$state = 'data';
} else {
/* Parse error. Switch to the bogus comment state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-closing-tag-but-got-char'
));
$this->token = array(
'data' => $char,
'type' => self::COMMENT
);
$state = 'bogus comment';
}
}
break;
case 'tag name':
/* Consume the next input character: */
$char = $this->stream->char();
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
Switch to the before attribute name state. */
$state = 'before attribute name';
} elseif($char === '/') {
/* U+002F SOLIDUS (/)
Switch to the self-closing start tag state. */
$state = 'self-closing start tag';
} elseif($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Emit the current tag token. Switch to the data state. */
$this->emitToken($this->token);
$state = 'data';
} elseif('A' <= $char && $char <= 'Z') {
/* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z
Append the lowercase version of the current input
character (add 0x0020 to the character's code point) to
the current tag token's tag name. Stay in the tag name state. */
$chars = $this->stream->charsWhile(self::UPPER_ALPHA);
$this->token['name'] .= strtolower($char . $chars);
$state = 'tag name';
} elseif($char === false) {
/* EOF
Parse error. Reconsume the EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-tag-name'
));
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Append the current input character to the current tag token's tag name.
Stay in the tag name state. */
$chars = $this->stream->charsUntil("\t\n\x0C />" . self::UPPER_ALPHA);
$this->token['name'] .= $char . $chars;
$state = 'tag name';
}
break;
case 'before attribute name':
/* Consume the next input character: */
$char = $this->stream->char();
// this conditional is optimized, check bottom
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
Stay in the before attribute name state. */
$state = 'before attribute name';
} elseif($char === '/') {
/* U+002F SOLIDUS (/)
Switch to the self-closing start tag state. */
$state = 'self-closing start tag';
} elseif($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Emit the current tag token. Switch to the data state. */
$this->emitToken($this->token);
$state = 'data';
} elseif('A' <= $char && $char <= 'Z') {
/* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z
Start a new attribute in the current tag token. Set that
attribute's name to the lowercase version of the current
input character (add 0x0020 to the character's code
point), and its value to the empty string. Switch to the
attribute name state.*/
$this->token['attr'][] = array(
'name' => strtolower($char),
'value' => ''
);
$state = 'attribute name';
} elseif($char === false) {
/* EOF
Parse error. Reconsume the EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-attribute-name-but-got-eof'
));
$this->stream->unget();
$state = 'data';
} else {
/* U+0022 QUOTATION MARK (")
U+0027 APOSTROPHE (')
U+003C LESS-THAN SIGN (<)
U+003D EQUALS SIGN (=)
Parse error. Treat it as per the "anything else" entry
below. */
if($char === '"' || $char === "'" || $char === '<' || $char === '=') {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'invalid-character-in-attribute-name'
));
}
/* Anything else
Start a new attribute in the current tag token. Set that attribute's
name to the current input character, and its value to the empty string.
Switch to the attribute name state. */
$this->token['attr'][] = array(
'name' => $char,
'value' => ''
);
$state = 'attribute name';
}
break;
case 'attribute name':
// Consume the next input character:
$char = $this->stream->char();
// this conditional is optimized, check bottom
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
Switch to the after attribute name state. */
$state = 'after attribute name';
} elseif($char === '/') {
/* U+002F SOLIDUS (/)
Switch to the self-closing start tag state. */
$state = 'self-closing start tag';
} elseif($char === '=') {
/* U+003D EQUALS SIGN (=)
Switch to the before attribute value state. */
$state = 'before attribute value';
} elseif($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Emit the current tag token. Switch to the data state. */
$this->emitToken($this->token);
$state = 'data';
} elseif('A' <= $char && $char <= 'Z') {
/* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z
Append the lowercase version of the current input
character (add 0x0020 to the character's code point) to
the current attribute's name. Stay in the attribute name
state. */
$chars = $this->stream->charsWhile(self::UPPER_ALPHA);
$last = count($this->token['attr']) - 1;
$this->token['attr'][$last]['name'] .= strtolower($char . $chars);
$state = 'attribute name';
} elseif($char === false) {
/* EOF
Parse error. Reconsume the EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-attribute-name'
));
$this->stream->unget();
$state = 'data';
} else {
/* U+0022 QUOTATION MARK (")
U+0027 APOSTROPHE (')
U+003C LESS-THAN SIGN (<)
Parse error. Treat it as per the "anything else"
entry below. */
if($char === '"' || $char === "'" || $char === '<') {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'invalid-character-in-attribute-name'
));
}
/* Anything else
Append the current input character to the current attribute's name.
Stay in the attribute name state. */
$chars = $this->stream->charsUntil("\t\n\x0C /=>\"'" . self::UPPER_ALPHA);
$last = count($this->token['attr']) - 1;
$this->token['attr'][$last]['name'] .= $char . $chars;
$state = 'attribute name';
}
/* When the user agent leaves the attribute name state
(and before emitting the tag token, if appropriate), the
complete attribute's name must be compared to the other
attributes on the same token; if there is already an
attribute on the token with the exact same name, then this
is a parse error and the new attribute must be dropped, along
with the value that gets associated with it (if any). */
// this might be implemented in the emitToken method
break;
case 'after attribute name':
// Consume the next input character:
$char = $this->stream->char();
// this is an optimized conditional, check the bottom
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
Stay in the after attribute name state. */
$state = 'after attribute name';
} elseif($char === '/') {
/* U+002F SOLIDUS (/)
Switch to the self-closing start tag state. */
$state = 'self-closing start tag';
} elseif($char === '=') {
/* U+003D EQUALS SIGN (=)
Switch to the before attribute value state. */
$state = 'before attribute value';
} elseif($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Emit the current tag token. Switch to the data state. */
$this->emitToken($this->token);
$state = 'data';
} elseif('A' <= $char && $char <= 'Z') {
/* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z
Start a new attribute in the current tag token. Set that
attribute's name to the lowercase version of the current
input character (add 0x0020 to the character's code
point), and its value to the empty string. Switch to the
attribute name state. */
$this->token['attr'][] = array(
'name' => strtolower($char),
'value' => ''
);
$state = 'attribute name';
} elseif($char === false) {
/* EOF
Parse error. Reconsume the EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-end-of-tag-but-got-eof'
));
$this->stream->unget();
$state = 'data';
} else {
/* U+0022 QUOTATION MARK (")
U+0027 APOSTROPHE (')
U+003C LESS-THAN SIGN(<)
Parse error. Treat it as per the "anything else"
entry below. */
if($char === '"' || $char === "'" || $char === "<") {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'invalid-character-after-attribute-name'
));
}
/* Anything else
Start a new attribute in the current tag token. Set that attribute's
name to the current input character, and its value to the empty string.
Switch to the attribute name state. */
$this->token['attr'][] = array(
'name' => $char,
'value' => ''
);
$state = 'attribute name';
}
break;
case 'before attribute value':
// Consume the next input character:
$char = $this->stream->char();
// this is an optimized conditional
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
Stay in the before attribute value state. */
$state = 'before attribute value';
} elseif($char === '"') {
/* U+0022 QUOTATION MARK (")
Switch to the attribute value (double-quoted) state. */
$state = 'attribute value (double-quoted)';
} elseif($char === '&') {
/* U+0026 AMPERSAND (&)
Switch to the attribute value (unquoted) state and reconsume
this input character. */
$this->stream->unget();
$state = 'attribute value (unquoted)';
} elseif($char === '\'') {
/* U+0027 APOSTROPHE (')
Switch to the attribute value (single-quoted) state. */
$state = 'attribute value (single-quoted)';
} elseif($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Parse error. Emit the current tag token. Switch to the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-attribute-value-but-got-right-bracket'
));
$this->emitToken($this->token);
$state = 'data';
} elseif($char === false) {
/* EOF
Parse error. Reconsume the EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-attribute-value-but-got-eof'
));
$this->stream->unget();
$state = 'data';
} else {
/* U+003D EQUALS SIGN (=)
* U+003C LESS-THAN SIGN (<)
Parse error. Treat it as per the "anything else" entry below. */
if($char === '=' || $char === '<') {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'equals-in-unquoted-attribute-value'
));
}
/* Anything else
Append the current input character to the current attribute's value.
Switch to the attribute value (unquoted) state. */
$last = count($this->token['attr']) - 1;
$this->token['attr'][$last]['value'] .= $char;
$state = 'attribute value (unquoted)';
}
break;
case 'attribute value (double-quoted)':
// Consume the next input character:
$char = $this->stream->char();
if($char === '"') {
/* U+0022 QUOTATION MARK (")
Switch to the after attribute value (quoted) state. */
$state = 'after attribute value (quoted)';
} elseif($char === '&') {
/* U+0026 AMPERSAND (&)
Switch to the character reference in attribute value
state, with the additional allowed character
being U+0022 QUOTATION MARK ("). */
$this->characterReferenceInAttributeValue('"');
} elseif($char === false) {
/* EOF
Parse error. Reconsume the EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-attribute-value-double-quote'
));
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Append the current input character to the current attribute's value.
Stay in the attribute value (double-quoted) state. */
$chars = $this->stream->charsUntil('"&');
$last = count($this->token['attr']) - 1;
$this->token['attr'][$last]['value'] .= $char . $chars;
$state = 'attribute value (double-quoted)';
}
break;
case 'attribute value (single-quoted)':
// Consume the next input character:
$char = $this->stream->char();
if($char === "'") {
/* U+0022 QUOTATION MARK (')
Switch to the after attribute value state. */
$state = 'after attribute value (quoted)';
} elseif($char === '&') {
/* U+0026 AMPERSAND (&)
Switch to the entity in attribute value state. */
$this->characterReferenceInAttributeValue("'");
} elseif($char === false) {
/* EOF
Parse error. Reconsume the EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-attribute-value-single-quote'
));
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Append the current input character to the current attribute's value.
Stay in the attribute value (single-quoted) state. */
$chars = $this->stream->charsUntil("'&");
$last = count($this->token['attr']) - 1;
$this->token['attr'][$last]['value'] .= $char . $chars;
$state = 'attribute value (single-quoted)';
}
break;
case 'attribute value (unquoted)':
// Consume the next input character:
$char = $this->stream->char();
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
Switch to the before attribute name state. */
$state = 'before attribute name';
} elseif($char === '&') {
/* U+0026 AMPERSAND (&)
Switch to the entity in attribute value state, with the
additional allowed character being U+003E
GREATER-THAN SIGN (>). */
$this->characterReferenceInAttributeValue('>');
} elseif($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Emit the current tag token. Switch to the data state. */
$this->emitToken($this->token);
$state = 'data';
} elseif ($char === false) {
/* EOF
Parse error. Reconsume the EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-attribute-value-no-quotes'
));
$this->stream->unget();
$state = 'data';
} else {
/* U+0022 QUOTATION MARK (")
U+0027 APOSTROPHE (')
U+003C LESS-THAN SIGN (<)
U+003D EQUALS SIGN (=)
Parse error. Treat it as per the "anything else"
entry below. */
if($char === '"' || $char === "'" || $char === '=' || $char == '<') {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-character-in-unquoted-attribute-value'
));
}
/* Anything else
Append the current input character to the current attribute's value.
Stay in the attribute value (unquoted) state. */
$chars = $this->stream->charsUntil("\t\n\x0c &>\"'=");
$last = count($this->token['attr']) - 1;
$this->token['attr'][$last]['value'] .= $char . $chars;
$state = 'attribute value (unquoted)';
}
break;
case 'after attribute value (quoted)':
/* Consume the next input character: */
$char = $this->stream->char();
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
Switch to the before attribute name state. */
$state = 'before attribute name';
} elseif ($char === '/') {
/* U+002F SOLIDUS (/)
Switch to the self-closing start tag state. */
$state = 'self-closing start tag';
} elseif ($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Emit the current tag token. Switch to the data state. */
$this->emitToken($this->token);
$state = 'data';
} elseif ($char === false) {
/* EOF
Parse error. Reconsume the EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-EOF-after-attribute-value'
));
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Parse error. Reconsume the character in the before attribute
name state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-character-after-attribute-value'
));
$this->stream->unget();
$state = 'before attribute name';
}
break;
case 'self-closing start tag':
/* Consume the next input character: */
$char = $this->stream->char();
if ($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Set the self-closing flag of the current tag token.
Emit the current tag token. Switch to the data state. */
// not sure if this is the name we want
$this->token['self-closing'] = true;
$this->emitToken($this->token);
$state = 'data';
} elseif ($char === false) {
/* EOF
Parse error. Reconsume the EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-eof-after-self-closing'
));
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Parse error. Reconsume the character in the before attribute name state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-character-after-self-closing'
));
$this->stream->unget();
$state = 'before attribute name';
}
break;
case 'bogus comment':
/* (This can only happen if the content model flag is set to the PCDATA state.) */
/* Consume every character up to the first U+003E GREATER-THAN SIGN
character (>) or the end of the file (EOF), whichever comes first. Emit
a comment token whose data is the concatenation of all the characters
starting from and including the character that caused the state machine
to switch into the bogus comment state, up to and including the last
consumed character before the U+003E character, if any, or up to the
end of the file otherwise. (If the comment was started by the end of
the file (EOF), the token is empty.) */
$this->token['data'] .= (string) $this->stream->charsUntil('>');
$this->stream->char();
$this->emitToken($this->token);
/* Switch to the data state. */
$state = 'data';
break;
case 'markup declaration open':
// Consume for below
$hyphens = $this->stream->charsWhile('-', 2);
if ($hyphens === '-') {
$this->stream->unget();
}
if ($hyphens !== '--') {
$alpha = $this->stream->charsWhile(self::ALPHA, 7);
}
/* If the next two characters are both U+002D HYPHEN-MINUS (-)
characters, consume those two characters, create a comment token whose
data is the empty string, and switch to the comment state. */
if($hyphens === '--') {
$state = 'comment start';
$this->token = array(
'data' => '',
'type' => self::COMMENT
);
/* Otherwise if the next seven characters are a case-insensitive match
for the word "DOCTYPE", then consume those characters and switch to the
DOCTYPE state. */
} elseif(strtoupper($alpha) === 'DOCTYPE') {
$state = 'DOCTYPE';
// XXX not implemented
/* Otherwise, if the insertion mode is "in foreign content"
and the current node is not an element in the HTML namespace
and the next seven characters are an ASCII case-sensitive
match for the string "[CDATA[" (the five uppercase letters
"CDATA" with a U+005B LEFT SQUARE BRACKET character before
and after), then consume those characters and switch to the
CDATA section state (which is unrelated to the content model
flag's CDATA state). */
/* Otherwise, is is a parse error. Switch to the bogus comment state.
The next character that is consumed, if any, is the first character
that will be in the comment. */
} else {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-dashes-or-doctype'
));
$this->token = array(
'data' => (string) $alpha,
'type' => self::COMMENT
);
$state = 'bogus comment';
}
break;
case 'comment start':
/* Consume the next input character: */
$char = $this->stream->char();
if ($char === '-') {
/* U+002D HYPHEN-MINUS (-)
Switch to the comment start dash state. */
$state = 'comment start dash';
} elseif ($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Parse error. Emit the comment token. Switch to the
data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'incorrect-comment'
));
$this->emitToken($this->token);
$state = 'data';
} elseif ($char === false) {
/* EOF
Parse error. Emit the comment token. Reconsume the
EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-comment'
));
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Append the input character to the comment token's
data. Switch to the comment state. */
$this->token['data'] .= $char;
$state = 'comment';
}
break;
case 'comment start dash':
/* Consume the next input character: */
$char = $this->stream->char();
if ($char === '-') {
/* U+002D HYPHEN-MINUS (-)
Switch to the comment end state */
$state = 'comment end';
} elseif ($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Parse error. Emit the comment token. Switch to the
data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'incorrect-comment'
));
$this->emitToken($this->token);
$state = 'data';
} elseif ($char === false) {
/* Parse error. Emit the comment token. Reconsume the
EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-comment'
));
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
$this->token['data'] .= '-' . $char;
$state = 'comment';
}
break;
case 'comment':
/* Consume the next input character: */
$char = $this->stream->char();
if($char === '-') {
/* U+002D HYPHEN-MINUS (-)
Switch to the comment end dash state */
$state = 'comment end dash';
} elseif($char === false) {
/* EOF
Parse error. Emit the comment token. Reconsume the EOF character
in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-comment'
));
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Append the input character to the comment token's data. Stay in
the comment state. */
$chars = $this->stream->charsUntil('-');
$this->token['data'] .= $char . $chars;
}
break;
case 'comment end dash':
/* Consume the next input character: */
$char = $this->stream->char();
if($char === '-') {
/* U+002D HYPHEN-MINUS (-)
Switch to the comment end state */
$state = 'comment end';
} elseif($char === false) {
/* EOF
Parse error. Emit the comment token. Reconsume the EOF character
in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-comment-end-dash'
));
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Append a U+002D HYPHEN-MINUS (-) character and the input
character to the comment token's data. Switch to the comment state. */
$this->token['data'] .= '-'.$char;
$state = 'comment';
}
break;
case 'comment end':
/* Consume the next input character: */
$char = $this->stream->char();
if($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Emit the comment token. Switch to the data state. */
$this->emitToken($this->token);
$state = 'data';
} elseif($char === '-') {
/* U+002D HYPHEN-MINUS (-)
Parse error. Append a U+002D HYPHEN-MINUS (-) character
to the comment token's data. Stay in the comment end
state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-dash-after-double-dash-in-comment'
));
$this->token['data'] .= '-';
} elseif($char === "\t" || $char === "\n" || $char === "\x0a" || $char === ' ') {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-space-after-double-dash-in-comment'
));
$this->token['data'] .= '--' . $char;
$state = 'comment end space';
} elseif($char === '!') {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-bang-after-double-dash-in-comment'
));
$state = 'comment end bang';
} elseif($char === false) {
/* EOF
Parse error. Emit the comment token. Reconsume the
EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-comment-double-dash'
));
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Parse error. Append two U+002D HYPHEN-MINUS (-)
characters and the input character to the comment token's
data. Switch to the comment state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-char-in-comment'
));
$this->token['data'] .= '--'.$char;
$state = 'comment';
}
break;
case 'comment end bang':
$char = $this->stream->char();
if ($char === '>') {
$this->emitToken($this->token);
$state = 'data';
} elseif ($char === "-") {
$this->token['data'] .= '--!';
$state = 'comment end dash';
} elseif ($char === false) {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-comment-end-bang'
));
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
$this->token['data'] .= '--!' . $char;
$state = 'comment';
}
break;
case 'comment end space':
$char = $this->stream->char();
if ($char === '>') {
$this->emitToken($this->token);
$state = 'data';
} elseif ($char === '-') {
$state = 'comment end dash';
} elseif ($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
$this->token['data'] .= $char;
} elseif ($char === false) {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-eof-in-comment-end-space',
));
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
$this->token['data'] .= $char;
$state = 'comment';
}
break;
case 'DOCTYPE':
/* Consume the next input character: */
$char = $this->stream->char();
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
Switch to the before DOCTYPE name state. */
$state = 'before DOCTYPE name';
} elseif($char === false) {
/* EOF
Parse error. Create a new DOCTYPE token. Set its
force-quirks flag to on. Emit the token. Reconsume the
EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'need-space-after-doctype-but-got-eof'
));
$this->emitToken(array(
'name' => '',
'type' => self::DOCTYPE,
'force-quirks' => true,
'error' => true
));
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Parse error. Reconsume the current character in the
before DOCTYPE name state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'need-space-after-doctype'
));
$this->stream->unget();
$state = 'before DOCTYPE name';
}
break;
case 'before DOCTYPE name':
/* Consume the next input character: */
$char = $this->stream->char();
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
Stay in the before DOCTYPE name state. */
} elseif($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Parse error. Create a new DOCTYPE token. Set its
force-quirks flag to on. Emit the token. Switch to the
data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-doctype-name-but-got-right-bracket'
));
$this->emitToken(array(
'name' => '',
'type' => self::DOCTYPE,
'force-quirks' => true,
'error' => true
));
$state = 'data';
} elseif('A' <= $char && $char <= 'Z') {
/* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z
Create a new DOCTYPE token. Set the token's name to the
lowercase version of the input character (add 0x0020 to
the character's code point). Switch to the DOCTYPE name
state. */
$this->token = array(
'name' => strtolower($char),
'type' => self::DOCTYPE,
'error' => true
);
$state = 'DOCTYPE name';
} elseif($char === false) {
/* EOF
Parse error. Create a new DOCTYPE token. Set its
force-quirks flag to on. Emit the token. Reconsume the
EOF character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-doctype-name-but-got-eof'
));
$this->emitToken(array(
'name' => '',
'type' => self::DOCTYPE,
'force-quirks' => true,
'error' => true
));
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Create a new DOCTYPE token. Set the token's name to the
current input character. Switch to the DOCTYPE name state. */
$this->token = array(
'name' => $char,
'type' => self::DOCTYPE,
'error' => true
);
$state = 'DOCTYPE name';
}
break;
case 'DOCTYPE name':
/* Consume the next input character: */
$char = $this->stream->char();
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
Switch to the after DOCTYPE name state. */
$state = 'after DOCTYPE name';
} elseif($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Emit the current DOCTYPE token. Switch to the data state. */
$this->emitToken($this->token);
$state = 'data';
} elseif('A' <= $char && $char <= 'Z') {
/* U+0041 LATIN CAPITAL LETTER A through to U+005A LATIN CAPITAL LETTER Z
Append the lowercase version of the input character
(add 0x0020 to the character's code point) to the current
DOCTYPE token's name. Stay in the DOCTYPE name state. */
$this->token['name'] .= strtolower($char);
} elseif($char === false) {
/* EOF
Parse error. Set the DOCTYPE token's force-quirks flag
to on. Emit that DOCTYPE token. Reconsume the EOF
character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-doctype-name'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Append the current input character to the current
DOCTYPE token's name. Stay in the DOCTYPE name state. */
$this->token['name'] .= $char;
}
// XXX this is probably some sort of quirks mode designation,
// check tree-builder to be sure. In general 'error' needs
// to be specc'ified, this probably means removing it at the end
$this->token['error'] = ($this->token['name'] === 'HTML')
? false
: true;
break;
case 'after DOCTYPE name':
/* Consume the next input character: */
$char = $this->stream->char();
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
Stay in the after DOCTYPE name state. */
} elseif($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Emit the current DOCTYPE token. Switch to the data state. */
$this->emitToken($this->token);
$state = 'data';
} elseif($char === false) {
/* EOF
Parse error. Set the DOCTYPE token's force-quirks flag
to on. Emit that DOCTYPE token. Reconsume the EOF
character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-doctype'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
/* Anything else */
$nextSix = strtoupper($char . $this->stream->charsWhile(self::ALPHA, 5));
if ($nextSix === 'PUBLIC') {
/* If the next six characters are an ASCII
case-insensitive match for the word "PUBLIC", then
consume those characters and switch to the before
DOCTYPE public identifier state. */
$state = 'before DOCTYPE public identifier';
} elseif ($nextSix === 'SYSTEM') {
/* Otherwise, if the next six characters are an ASCII
case-insensitive match for the word "SYSTEM", then
consume those characters and switch to the before
DOCTYPE system identifier state. */
$state = 'before DOCTYPE system identifier';
} else {
/* Otherwise, this is the parse error. Set the DOCTYPE
token's force-quirks flag to on. Switch to the bogus
DOCTYPE state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-space-or-right-bracket-in-doctype'
));
$this->token['force-quirks'] = true;
$this->token['error'] = true;
$state = 'bogus DOCTYPE';
}
}
break;
case 'before DOCTYPE public identifier':
/* Consume the next input character: */
$char = $this->stream->char();
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
Stay in the before DOCTYPE public identifier state. */
} elseif ($char === '"') {
/* U+0022 QUOTATION MARK (")
Set the DOCTYPE token's public identifier to the empty
string (not missing), then switch to the DOCTYPE public
identifier (double-quoted) state. */
$this->token['public'] = '';
$state = 'DOCTYPE public identifier (double-quoted)';
} elseif ($char === "'") {
/* U+0027 APOSTROPHE (')
Set the DOCTYPE token's public identifier to the empty
string (not missing), then switch to the DOCTYPE public
identifier (single-quoted) state. */
$this->token['public'] = '';
$state = 'DOCTYPE public identifier (single-quoted)';
} elseif ($char === '>') {
/* Parse error. Set the DOCTYPE token's force-quirks flag
to on. Emit that DOCTYPE token. Switch to the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-end-of-doctype'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$state = 'data';
} elseif ($char === false) {
/* Parse error. Set the DOCTYPE token's force-quirks
flag to on. Emit that DOCTYPE token. Reconsume the EOF
character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-doctype'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
/* Parse error. Set the DOCTYPE token's force-quirks flag
to on. Switch to the bogus DOCTYPE state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-char-in-doctype'
));
$this->token['force-quirks'] = true;
$state = 'bogus DOCTYPE';
}
break;
case 'DOCTYPE public identifier (double-quoted)':
/* Consume the next input character: */
$char = $this->stream->char();
if ($char === '"') {
/* U+0022 QUOTATION MARK (")
Switch to the after DOCTYPE public identifier state. */
$state = 'after DOCTYPE public identifier';
} elseif ($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Parse error. Set the DOCTYPE token's force-quirks flag
to on. Emit that DOCTYPE token. Switch to the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-end-of-doctype'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$state = 'data';
} elseif ($char === false) {
/* EOF
Parse error. Set the DOCTYPE token's force-quirks flag
to on. Emit that DOCTYPE token. Reconsume the EOF
character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-doctype'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Append the current input character to the current
DOCTYPE token's public identifier. Stay in the DOCTYPE
public identifier (double-quoted) state. */
$this->token['public'] .= $char;
}
break;
case 'DOCTYPE public identifier (single-quoted)':
/* Consume the next input character: */
$char = $this->stream->char();
if ($char === "'") {
/* U+0027 APOSTROPHE (')
Switch to the after DOCTYPE public identifier state. */
$state = 'after DOCTYPE public identifier';
} elseif ($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Parse error. Set the DOCTYPE token's force-quirks flag
to on. Emit that DOCTYPE token. Switch to the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-end-of-doctype'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$state = 'data';
} elseif ($char === false) {
/* EOF
Parse error. Set the DOCTYPE token's force-quirks flag
to on. Emit that DOCTYPE token. Reconsume the EOF
character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-doctype'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Append the current input character to the current
DOCTYPE token's public identifier. Stay in the DOCTYPE
public identifier (double-quoted) state. */
$this->token['public'] .= $char;
}
break;
case 'after DOCTYPE public identifier':
/* Consume the next input character: */
$char = $this->stream->char();
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
Stay in the after DOCTYPE public identifier state. */
} elseif ($char === '"') {
/* U+0022 QUOTATION MARK (")
Set the DOCTYPE token's system identifier to the
empty string (not missing), then switch to the DOCTYPE
system identifier (double-quoted) state. */
$this->token['system'] = '';
$state = 'DOCTYPE system identifier (double-quoted)';
} elseif ($char === "'") {
/* U+0027 APOSTROPHE (')
Set the DOCTYPE token's system identifier to the
empty string (not missing), then switch to the DOCTYPE
system identifier (single-quoted) state. */
$this->token['system'] = '';
$state = 'DOCTYPE system identifier (single-quoted)';
} elseif ($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Emit the current DOCTYPE token. Switch to the data state. */
$this->emitToken($this->token);
$state = 'data';
} elseif ($char === false) {
/* Parse error. Set the DOCTYPE token's force-quirks
flag to on. Emit that DOCTYPE token. Reconsume the EOF
character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-doctype'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Parse error. Set the DOCTYPE token's force-quirks flag
to on. Switch to the bogus DOCTYPE state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-char-in-doctype'
));
$this->token['force-quirks'] = true;
$state = 'bogus DOCTYPE';
}
break;
case 'before DOCTYPE system identifier':
/* Consume the next input character: */
$char = $this->stream->char();
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
Stay in the before DOCTYPE system identifier state. */
} elseif ($char === '"') {
/* U+0022 QUOTATION MARK (")
Set the DOCTYPE token's system identifier to the empty
string (not missing), then switch to the DOCTYPE system
identifier (double-quoted) state. */
$this->token['system'] = '';
$state = 'DOCTYPE system identifier (double-quoted)';
} elseif ($char === "'") {
/* U+0027 APOSTROPHE (')
Set the DOCTYPE token's system identifier to the empty
string (not missing), then switch to the DOCTYPE system
identifier (single-quoted) state. */
$this->token['system'] = '';
$state = 'DOCTYPE system identifier (single-quoted)';
} elseif ($char === '>') {
/* Parse error. Set the DOCTYPE token's force-quirks flag
to on. Emit that DOCTYPE token. Switch to the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-char-in-doctype'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$state = 'data';
} elseif ($char === false) {
/* Parse error. Set the DOCTYPE token's force-quirks
flag to on. Emit that DOCTYPE token. Reconsume the EOF
character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-doctype'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
/* Parse error. Set the DOCTYPE token's force-quirks flag
to on. Switch to the bogus DOCTYPE state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-char-in-doctype'
));
$this->token['force-quirks'] = true;
$state = 'bogus DOCTYPE';
}
break;
case 'DOCTYPE system identifier (double-quoted)':
/* Consume the next input character: */
$char = $this->stream->char();
if ($char === '"') {
/* U+0022 QUOTATION MARK (")
Switch to the after DOCTYPE system identifier state. */
$state = 'after DOCTYPE system identifier';
} elseif ($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Parse error. Set the DOCTYPE token's force-quirks flag
to on. Emit that DOCTYPE token. Switch to the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-end-of-doctype'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$state = 'data';
} elseif ($char === false) {
/* EOF
Parse error. Set the DOCTYPE token's force-quirks flag
to on. Emit that DOCTYPE token. Reconsume the EOF
character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-doctype'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Append the current input character to the current
DOCTYPE token's system identifier. Stay in the DOCTYPE
system identifier (double-quoted) state. */
$this->token['system'] .= $char;
}
break;
case 'DOCTYPE system identifier (single-quoted)':
/* Consume the next input character: */
$char = $this->stream->char();
if ($char === "'") {
/* U+0027 APOSTROPHE (')
Switch to the after DOCTYPE system identifier state. */
$state = 'after DOCTYPE system identifier';
} elseif ($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Parse error. Set the DOCTYPE token's force-quirks flag
to on. Emit that DOCTYPE token. Switch to the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-end-of-doctype'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$state = 'data';
} elseif ($char === false) {
/* EOF
Parse error. Set the DOCTYPE token's force-quirks flag
to on. Emit that DOCTYPE token. Reconsume the EOF
character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-doctype'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Append the current input character to the current
DOCTYPE token's system identifier. Stay in the DOCTYPE
system identifier (double-quoted) state. */
$this->token['system'] .= $char;
}
break;
case 'after DOCTYPE system identifier':
/* Consume the next input character: */
$char = $this->stream->char();
if($char === "\t" || $char === "\n" || $char === "\x0c" || $char === ' ') {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
Stay in the after DOCTYPE system identifier state. */
} elseif ($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Emit the current DOCTYPE token. Switch to the data state. */
$this->emitToken($this->token);
$state = 'data';
} elseif ($char === false) {
/* Parse error. Set the DOCTYPE token's force-quirks
flag to on. Emit that DOCTYPE token. Reconsume the EOF
character in the data state. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'eof-in-doctype'
));
$this->token['force-quirks'] = true;
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Parse error. Switch to the bogus DOCTYPE state.
(This does not set the DOCTYPE token's force-quirks
flag to on.) */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'unexpected-char-in-doctype'
));
$state = 'bogus DOCTYPE';
}
break;
case 'bogus DOCTYPE':
/* Consume the next input character: */
$char = $this->stream->char();
if ($char === '>') {
/* U+003E GREATER-THAN SIGN (>)
Emit the DOCTYPE token. Switch to the data state. */
$this->emitToken($this->token);
$state = 'data';
} elseif($char === false) {
/* EOF
Emit the DOCTYPE token. Reconsume the EOF character in
the data state. */
$this->emitToken($this->token);
$this->stream->unget();
$state = 'data';
} else {
/* Anything else
Stay in the bogus DOCTYPE state. */
}
break;
// case 'cdataSection':
}
}
}
/**
* Returns a serialized representation of the tree.
*/
public function save() {
return $this->tree->save();
}
/**
* Returns the input stream.
*/
public function stream() {
return $this->stream;
}
private function consumeCharacterReference($allowed = false, $inattr = false) {
// This goes quite far against spec, and is far closer to the Python
// impl., mainly because we don't do the large unconsuming the spec
// requires.
// All consumed characters.
$chars = $this->stream->char();
/* This section defines how to consume a character
reference. This definition is used when parsing character
references in text and in attributes.
The behavior depends on the identity of the next character
(the one immediately after the U+0026 AMPERSAND character): */
if (
$chars[0] === "\x09" ||
$chars[0] === "\x0A" ||
$chars[0] === "\x0C" ||
$chars[0] === "\x20" ||
$chars[0] === '<' ||
$chars[0] === '&' ||
$chars === false ||
$chars[0] === $allowed
) {
/* U+0009 CHARACTER TABULATION
U+000A LINE FEED (LF)
U+000C FORM FEED (FF)
U+0020 SPACE
U+003C LESS-THAN SIGN
U+0026 AMPERSAND
EOF
The additional allowed character, if there is one
Not a character reference. No characters are consumed,
and nothing is returned. (This is not an error, either.) */
// We already consumed, so unconsume.
$this->stream->unget();
return '&';
} elseif ($chars[0] === '#') {
/* Consume the U+0023 NUMBER SIGN. */
// Um, yeah, we already did that.
/* The behavior further depends on the character after
the U+0023 NUMBER SIGN: */
$chars .= $this->stream->char();
if (isset($chars[1]) && ($chars[1] === 'x' || $chars[1] === 'X')) {
/* U+0078 LATIN SMALL LETTER X
U+0058 LATIN CAPITAL LETTER X */
/* Consume the X. */
// Um, yeah, we already did that.
/* Follow the steps below, but using the range of
characters U+0030 DIGIT ZERO through to U+0039 DIGIT
NINE, U+0061 LATIN SMALL LETTER A through to U+0066
LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER
A, through to U+0046 LATIN CAPITAL LETTER F (in other
words, 0123456789, ABCDEF, abcdef). */
$char_class = self::HEX;
/* When it comes to interpreting the
number, interpret it as a hexadecimal number. */
$hex = true;
} else {
/* Anything else */
// Unconsume because we shouldn't have consumed this.
$chars = $chars[0];
$this->stream->unget();
/* Follow the steps below, but using the range of
characters U+0030 DIGIT ZERO through to U+0039 DIGIT
NINE (i.e. just 0123456789). */
$char_class = self::DIGIT;
/* When it comes to interpreting the number,
interpret it as a decimal number. */
$hex = false;
}
/* Consume as many characters as match the range of characters given above. */
$consumed = $this->stream->charsWhile($char_class);
if ($consumed === '' || $consumed === false) {
/* If no characters match the range, then don't consume
any characters (and unconsume the U+0023 NUMBER SIGN
character and, if appropriate, the X character). This
is a parse error; nothing is returned. */
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-numeric-entity'
));
return '&' . $chars;
} else {
/* Otherwise, if the next character is a U+003B SEMICOLON,
consume that too. If it isn't, there is a parse error. */
if ($this->stream->char() !== ';') {
$this->stream->unget();
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'numeric-entity-without-semicolon'
));
}
/* If one or more characters match the range, then take
them all and interpret the string of characters as a number
(either hexadecimal or decimal as appropriate). */
$codepoint = $hex ? hexdec($consumed) : (int) $consumed;
/* If that number is one of the numbers in the first column
of the following table, then this is a parse error. Find the
row with that number in the first column, and return a
character token for the Unicode character given in the
second column of that row. */
$new_codepoint = HTML5_Data::getRealCodepoint($codepoint);
if ($new_codepoint) {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'illegal-windows-1252-entity'
));
return HTML5_Data::utf8chr($new_codepoint);
} else {
/* Otherwise, if the number is greater than 0x10FFFF, then
* this is a parse error. Return a U+FFFD REPLACEMENT
* CHARACTER. */
if ($codepoint > 0x10FFFF) {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'overlong-character-entity' // XXX probably not correct
));
return "\xEF\xBF\xBD";
}
/* Otherwise, return a character token for the Unicode
* character whose code point is that number. If the
* number is in the range 0x0001 to 0x0008, 0x000E to
* 0x001F, 0x007F to 0x009F, 0xD800 to 0xDFFF, 0xFDD0 to
* 0xFDEF, or is one of 0x000B, 0xFFFE, 0xFFFF, 0x1FFFE,
* 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0x3FFFF, 0x4FFFE,
* 0x4FFFF, 0x5FFFE, 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE,
* 0x7FFFF, 0x8FFFE, 0x8FFFF, 0x9FFFE, 0x9FFFF, 0xAFFFE,
* 0xAFFFF, 0xBFFFE, 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE,
* 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, 0x10FFFE,
* or 0x10FFFF, then this is a parse error. */
// && has higher precedence than ||
if (
$codepoint >= 0x0000 && $codepoint <= 0x0008 ||
$codepoint === 0x000B ||
$codepoint >= 0x000E && $codepoint <= 0x001F ||
$codepoint >= 0x007F && $codepoint <= 0x009F ||
$codepoint >= 0xD800 && $codepoint <= 0xDFFF ||
$codepoint >= 0xFDD0 && $codepoint <= 0xFDEF ||
($codepoint & 0xFFFE) === 0xFFFE ||
$codepoint == 0x10FFFF || $codepoint == 0x10FFFE
) {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'illegal-codepoint-for-numeric-entity'
));
}
return HTML5_Data::utf8chr($codepoint);
}
}
} else {
/* Anything else */
/* Consume the maximum number of characters possible,
with the consumed characters matching one of the
identifiers in the first column of the named character
references table (in a case-sensitive manner). */
// What we actually do here is consume as much as we can while it
// matches the start of one of the identifiers in the first column.
$refs = HTML5_Data::getNamedCharacterReferences();
// Get the longest string which is the start of an identifier
// ($chars) as well as the longest identifier which matches ($id)
// and its codepoint ($codepoint).
$codepoint = false;
$char = $chars;
while ($char !== false && isset($refs[$char])) {
$refs = $refs[$char];
if (isset($refs['codepoint'])) {
$id = $chars;
$codepoint = $refs['codepoint'];
}
$chars .= $char = $this->stream->char();
}
// Unconsume the one character we just took which caused the while
// statement to fail. This could be anything and could cause state
// changes (as if it matches the while loop it must be
// alphanumeric so we can just concat it to whatever we get later).
$this->stream->unget();
if ($char !== false) {
$chars = substr($chars, 0, -1);
}
/* If no match can be made, then this is a parse error.
No characters are consumed, and nothing is returned. */
if (!$codepoint) {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'expected-named-entity'
));
return '&' . $chars;
}
/* If the last character matched is not a U+003B SEMICOLON
(;), there is a parse error. */
$semicolon = true;
if (substr($id, -1) !== ';') {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'named-entity-without-semicolon'
));
$semicolon = false;
}
/* If the character reference is being consumed as part of
an attribute, and the last character matched is not a
U+003B SEMICOLON (;), and the next character is in the
range U+0030 DIGIT ZERO to U+0039 DIGIT NINE, U+0041
LATIN CAPITAL LETTER A to U+005A LATIN CAPITAL LETTER Z,
or U+0061 LATIN SMALL LETTER A to U+007A LATIN SMALL LETTER Z,
then, for historical reasons, all the characters that were
matched after the U+0026 AMPERSAND (&) must be unconsumed,
and nothing is returned. */
if ($inattr && !$semicolon) {
// The next character is either the next character in $chars or in the stream.
if (strlen($chars) > strlen($id)) {
$next = substr($chars, strlen($id), 1);
} else {
$next = $this->stream->char();
$this->stream->unget();
}
if (
'0' <= $next && $next <= '9' ||
'A' <= $next && $next <= 'Z' ||
'a' <= $next && $next <= 'z'
) {
return '&' . $chars;
}
}
/* Otherwise, return a character token for the character
corresponding to the character reference name (as given
by the second column of the named character references table). */
return HTML5_Data::utf8chr($codepoint) . substr($chars, strlen($id));
}
}
private function characterReferenceInAttributeValue($allowed = false) {
/* Attempt to consume a character reference. */
$entity = $this->consumeCharacterReference($allowed, true);
/* If nothing is returned, append a U+0026 AMPERSAND
character to the current attribute's value.
Otherwise, append the returned character token to the
current attribute's value. */
$char = (!$entity)
? '&'
: $entity;
$last = count($this->token['attr']) - 1;
$this->token['attr'][$last]['value'] .= $char;
/* Finally, switch back to the attribute value state that you
were in when were switched into this state. */
}
/**
* Emits a token, passing it on to the tree builder.
*/
protected function emitToken($token, $checkStream = true, $dry = false) {
if ($checkStream) {
// Emit errors from input stream.
while ($this->stream->errors) {
$this->emitToken(array_shift($this->stream->errors), false);
}
}
if($token['type'] === self::ENDTAG && !empty($token['attr'])) {
for ($i = 0; $i < count($token['attr']); $i++) {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'attributes-in-end-tag'
));
}
}
if($token['type'] === self::ENDTAG && !empty($token['self-closing'])) {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'self-closing-flag-on-end-tag',
));
}
if($token['type'] === self::STARTTAG) {
// This could be changed to actually pass the tree-builder a hash
$hash = array();
foreach ($token['attr'] as $keypair) {
if (isset($hash[$keypair['name']])) {
$this->emitToken(array(
'type' => self::PARSEERROR,
'data' => 'duplicate-attribute',
));
} else {
$hash[$keypair['name']] = $keypair['value'];
}
}
}
if(!$dry) {
// the current structure of attributes is not a terribly good one
$this->tree->emitToken($token);
}
if(!$dry && is_int($this->tree->content_model)) {
$this->content_model = $this->tree->content_model;
$this->tree->content_model = null;
} elseif($token['type'] === self::ENDTAG) {
$this->content_model = self::PCDATA;
}
}
}

3840
thirdparty/html5lib/HTML5/TreeBuilder.php vendored Normal file
View File

@ -0,0 +1,3840 @@
<?php
/*
Copyright 2007 Jeroen van der Meer <http://jero.net/>
Copyright 2009 Edward Z. Yang <edwardzyang@thewritingpot.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// Tags for FIX ME!!!: (in order of priority)
// XXX - should be fixed NAO!
// XERROR - with regards to parse errors
// XSCRIPT - with regards to scripting mode
// XENCODING - with regards to encoding (for reparsing tests)
// XDOM - DOM specific code (tagName is explicitly not marked).
// this is not (yet) in helper functions.
class HTML5_TreeBuilder {
public $stack = array();
public $content_model;
private $mode;
private $original_mode;
private $secondary_mode;
private $dom;
// Whether or not normal insertion of nodes should actually foster
// parent (used in one case in spec)
private $foster_parent = false;
private $a_formatting = array();
private $head_pointer = null;
private $form_pointer = null;
private $flag_frameset_ok = true;
private $flag_force_quirks = false;
private $ignored = false;
private $quirks_mode = null;
// this gets to 2 when we want to ignore the next lf character, and
// is decrement at the beginning of each processed token (this way,
// code can check for (bool)$ignore_lf_token, but it phases out
// appropriately)
private $ignore_lf_token = 0;
private $fragment = false;
private $root;
private $scoping = array('applet','button','caption','html','marquee','object','table','td','th', 'svg:foreignObject');
private $formatting = array('a','b','big','code','em','font','i','nobr','s','small','strike','strong','tt','u');
// dl and ds are speculative
private $special = array('address','area','article','aside','base','basefont','bgsound',
'blockquote','body','br','center','col','colgroup','command','dc','dd','details','dir','div','dl','ds',
'dt','embed','fieldset','figure','footer','form','frame','frameset','h1','h2','h3','h4','h5',
'h6','head','header','hgroup','hr','iframe','img','input','isindex','li','link',
'listing','menu','meta','nav','noembed','noframes','noscript','ol',
'p','param','plaintext','pre','script','select','spacer','style',
'tbody','textarea','tfoot','thead','title','tr','ul','wbr');
private $pendingTableCharacters;
private $pendingTableCharactersDirty;
// Tree construction modes
const INITIAL = 0;
const BEFORE_HTML = 1;
const BEFORE_HEAD = 2;
const IN_HEAD = 3;
const IN_HEAD_NOSCRIPT = 4;
const AFTER_HEAD = 5;
const IN_BODY = 6;
const IN_CDATA_RCDATA = 7;
const IN_TABLE = 8;
const IN_TABLE_TEXT = 9;
const IN_CAPTION = 10;
const IN_COLUMN_GROUP = 11;
const IN_TABLE_BODY = 12;
const IN_ROW = 13;
const IN_CELL = 14;
const IN_SELECT = 15;
const IN_SELECT_IN_TABLE= 16;
const IN_FOREIGN_CONTENT= 17;
const AFTER_BODY = 18;
const IN_FRAMESET = 19;
const AFTER_FRAMESET = 20;
const AFTER_AFTER_BODY = 21;
const AFTER_AFTER_FRAMESET = 22;
/**
* Converts a magic number to a readable name. Use for debugging.
*/
private function strConst($number) {
static $lookup;
if (!$lookup) {
$lookup = array();
$r = new ReflectionClass('HTML5_TreeBuilder');
$consts = $r->getConstants();
foreach ($consts as $const => $num) {
if (!is_int($num)) continue;
$lookup[$num] = $const;
}
}
return $lookup[$number];
}
// The different types of elements.
const SPECIAL = 100;
const SCOPING = 101;
const FORMATTING = 102;
const PHRASING = 103;
// Quirks modes in $quirks_mode
const NO_QUIRKS = 200;
const QUIRKS_MODE = 201;
const LIMITED_QUIRKS_MODE = 202;
// Marker to be placed in $a_formatting
const MARKER = 300;
// Namespaces for foreign content
const NS_HTML = null; // to prevent DOM from requiring NS on everything
const NS_MATHML = 'http://www.w3.org/1998/Math/MathML';
const NS_SVG = 'http://www.w3.org/2000/svg';
const NS_XLINK = 'http://www.w3.org/1999/xlink';
const NS_XML = 'http://www.w3.org/XML/1998/namespace';
const NS_XMLNS = 'http://www.w3.org/2000/xmlns/';
// Different types of scopes to test for elements
const SCOPE = 0;
const SCOPE_LISTITEM = 1;
const SCOPE_TABLE = 2;
public function __construct() {
$this->mode = self::INITIAL;
$this->dom = new DOMDocument;
$this->dom->encoding = 'UTF-8';
$this->dom->preserveWhiteSpace = true;
$this->dom->substituteEntities = true;
$this->dom->strictErrorChecking = false;
}
// Process tag tokens
public function emitToken($token, $mode = null) {
// XXX: ignore parse errors... why are we emitting them, again?
if ($token['type'] === HTML5_Tokenizer::PARSEERROR) return;
if ($mode === null) $mode = $this->mode;
/*
$backtrace = debug_backtrace();
if ($backtrace[1]['class'] !== 'HTML5_TreeBuilder') echo "--\n";
echo $this->strConst($mode);
if ($this->original_mode) echo " (originally ".$this->strConst($this->original_mode).")";
echo "\n ";
token_dump($token);
$this->printStack();
$this->printActiveFormattingElements();
if ($this->foster_parent) echo " -> this is a foster parent mode\n";
if ($this->flag_frameset_ok) echo " -> frameset ok\n";
*/
if ($this->ignore_lf_token) $this->ignore_lf_token--;
$this->ignored = false;
// indenting is a little wonky, this can be changed later on
switch ($mode) {
case self::INITIAL:
/* A character token that is one of U+0009 CHARACTER TABULATION,
* U+000A LINE FEED (LF), U+000C FORM FEED (FF), or U+0020 SPACE */
if ($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
/* Ignore the token. */
$this->ignored = true;
} elseif ($token['type'] === HTML5_Tokenizer::DOCTYPE) {
if (
$token['name'] !== 'html' || !empty($token['public']) ||
!empty($token['system']) || $token !== 'about:legacy-compat'
) {
/* If the DOCTYPE token's name is not a case-sensitive match
* for the string "html", or if the token's public identifier
* is not missing, or if the token's system identifier is
* neither missing nor a case-sensitive match for the string
* "about:legacy-compat", then there is a parse error (this
* is the DOCTYPE parse error). */
// DOCTYPE parse error
}
/* Append a DocumentType node to the Document node, with the name
* attribute set to the name given in the DOCTYPE token, or the
* empty string if the name was missing; the publicId attribute
* set to the public identifier given in the DOCTYPE token, or
* the empty string if the public identifier was missing; the
* systemId attribute set to the system identifier given in the
* DOCTYPE token, or the empty string if the system identifier
* was missing; and the other attributes specific to
* DocumentType objects set to null and empty lists as
* appropriate. Associate the DocumentType node with the
* Document object so that it is returned as the value of the
* doctype attribute of the Document object. */
if (!isset($token['public'])) $token['public'] = null;
if (!isset($token['system'])) $token['system'] = null;
// XDOM
// Yes this is hacky. I'm kind of annoyed that I can't appendChild
// a doctype to DOMDocument. Maybe I haven't chanted the right
// syllables.
$impl = new DOMImplementation();
// This call can fail for particularly pathological cases (namely,
// the qualifiedName parameter ($token['name']) could be missing.
if ($token['name']) {
$doctype = $impl->createDocumentType($token['name'], $token['public'], $token['system']);
$this->dom->appendChild($doctype);
} else {
// It looks like libxml's not actually *able* to express this case.
// So... don't.
$this->dom->emptyDoctype = true;
}
$public = is_null($token['public']) ? false : strtolower($token['public']);
$system = is_null($token['system']) ? false : strtolower($token['system']);
$publicStartsWithForQuirks = array(
"+//silmaril//dtd html pro v0r11 19970101//",
"-//advasoft ltd//dtd html 3.0 aswedit + extensions//",
"-//as//dtd html 3.0 aswedit + extensions//",
"-//ietf//dtd html 2.0 level 1//",
"-//ietf//dtd html 2.0 level 2//",
"-//ietf//dtd html 2.0 strict level 1//",
"-//ietf//dtd html 2.0 strict level 2//",
"-//ietf//dtd html 2.0 strict//",
"-//ietf//dtd html 2.0//",
"-//ietf//dtd html 2.1e//",
"-//ietf//dtd html 3.0//",
"-//ietf//dtd html 3.2 final//",
"-//ietf//dtd html 3.2//",
"-//ietf//dtd html 3//",
"-//ietf//dtd html level 0//",
"-//ietf//dtd html level 1//",
"-//ietf//dtd html level 2//",
"-//ietf//dtd html level 3//",
"-//ietf//dtd html strict level 0//",
"-//ietf//dtd html strict level 1//",
"-//ietf//dtd html strict level 2//",
"-//ietf//dtd html strict level 3//",
"-//ietf//dtd html strict//",
"-//ietf//dtd html//",
"-//metrius//dtd metrius presentational//",
"-//microsoft//dtd internet explorer 2.0 html strict//",
"-//microsoft//dtd internet explorer 2.0 html//",
"-//microsoft//dtd internet explorer 2.0 tables//",
"-//microsoft//dtd internet explorer 3.0 html strict//",
"-//microsoft//dtd internet explorer 3.0 html//",
"-//microsoft//dtd internet explorer 3.0 tables//",
"-//netscape comm. corp.//dtd html//",
"-//netscape comm. corp.//dtd strict html//",
"-//o'reilly and associates//dtd html 2.0//",
"-//o'reilly and associates//dtd html extended 1.0//",
"-//o'reilly and associates//dtd html extended relaxed 1.0//",
"-//spyglass//dtd html 2.0 extended//",
"-//sq//dtd html 2.0 hotmetal + extensions//",
"-//sun microsystems corp.//dtd hotjava html//",
"-//sun microsystems corp.//dtd hotjava strict html//",
"-//w3c//dtd html 3 1995-03-24//",
"-//w3c//dtd html 3.2 draft//",
"-//w3c//dtd html 3.2 final//",
"-//w3c//dtd html 3.2//",
"-//w3c//dtd html 3.2s draft//",
"-//w3c//dtd html 4.0 frameset//",
"-//w3c//dtd html 4.0 transitional//",
"-//w3c//dtd html experimental 19960712//",
"-//w3c//dtd html experimental 970421//",
"-//w3c//dtd w3 html//",
"-//w3o//dtd w3 html 3.0//",
"-//webtechs//dtd mozilla html 2.0//",
"-//webtechs//dtd mozilla html//",
);
$publicSetToForQuirks = array(
"-//w3o//dtd w3 html strict 3.0//",
"-/w3c/dtd html 4.0 transitional/en",
"html",
);
$publicStartsWithAndSystemForQuirks = array(
"-//w3c//dtd html 4.01 frameset//",
"-//w3c//dtd html 4.01 transitional//",
);
$publicStartsWithForLimitedQuirks = array(
"-//w3c//dtd xhtml 1.0 frameset//",
"-//w3c//dtd xhtml 1.0 transitional//",
);
$publicStartsWithAndSystemForLimitedQuirks = array(
"-//w3c//dtd html 4.01 frameset//",
"-//w3c//dtd html 4.01 transitional//",
);
// first, do easy checks
if (
!empty($token['force-quirks']) ||
strtolower($token['name']) !== 'html'
) {
$this->quirks_mode = self::QUIRKS_MODE;
} else {
do {
if ($system) {
foreach ($publicStartsWithAndSystemForQuirks as $x) {
if (strncmp($public, $x, strlen($x)) === 0) {
$this->quirks_mode = self::QUIRKS_MODE;
break;
}
}
if (!is_null($this->quirks_mode)) break;
foreach ($publicStartsWithAndSystemForLimitedQuirks as $x) {
if (strncmp($public, $x, strlen($x)) === 0) {
$this->quirks_mode = self::LIMITED_QUIRKS_MODE;
break;
}
}
if (!is_null($this->quirks_mode)) break;
}
foreach ($publicSetToForQuirks as $x) {
if ($public === $x) {
$this->quirks_mode = self::QUIRKS_MODE;
break;
}
}
if (!is_null($this->quirks_mode)) break;
foreach ($publicStartsWithForLimitedQuirks as $x) {
if (strncmp($public, $x, strlen($x)) === 0) {
$this->quirks_mode = self::LIMITED_QUIRKS_MODE;
}
}
if (!is_null($this->quirks_mode)) break;
if ($system === "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd") {
$this->quirks_mode = self::QUIRKS_MODE;
break;
}
foreach ($publicStartsWithForQuirks as $x) {
if (strncmp($public, $x, strlen($x)) === 0) {
$this->quirks_mode = self::QUIRKS_MODE;
break;
}
}
if (is_null($this->quirks_mode)) {
$this->quirks_mode = self::NO_QUIRKS;
}
} while (false);
}
$this->mode = self::BEFORE_HTML;
} else {
// parse error
/* Switch the insertion mode to "before html", then reprocess the
* current token. */
$this->mode = self::BEFORE_HTML;
$this->quirks_mode = self::QUIRKS_MODE;
$this->emitToken($token);
}
break;
case self::BEFORE_HTML:
/* A DOCTYPE token */
if($token['type'] === HTML5_Tokenizer::DOCTYPE) {
// Parse error. Ignore the token.
$this->ignored = true;
/* A comment token */
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
/* Append a Comment node to the Document object with the data
attribute set to the data given in the comment token. */
// XDOM
$comment = $this->dom->createComment($token['data']);
$this->dom->appendChild($comment);
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
or U+0020 SPACE */
} elseif($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
/* Ignore the token. */
$this->ignored = true;
/* A start tag whose tag name is "html" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] == 'html') {
/* Create an element for the token in the HTML namespace. Append it
* to the Document object. Put this element in the stack of open
* elements. */
// XDOM
$html = $this->insertElement($token, false);
$this->dom->appendChild($html);
$this->stack[] = $html;
$this->mode = self::BEFORE_HEAD;
} else {
/* Create an html element. Append it to the Document object. Put
* this element in the stack of open elements. */
// XDOM
$html = $this->dom->createElementNS(self::NS_HTML, 'html');
$this->dom->appendChild($html);
$this->stack[] = $html;
/* Switch the insertion mode to "before head", then reprocess the
* current token. */
$this->mode = self::BEFORE_HEAD;
$this->emitToken($token);
}
break;
case self::BEFORE_HEAD:
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
or U+0020 SPACE */
if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
/* Ignore the token. */
$this->ignored = true;
/* A comment token */
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
/* Append a Comment node to the current node with the data attribute
set to the data given in the comment token. */
$this->insertComment($token['data']);
/* A DOCTYPE token */
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
/* Parse error. Ignore the token */
$this->ignored = true;
// parse error
/* A start tag token with the tag name "html" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
/* Process the token using the rules for the "in body"
* insertion mode. */
$this->processWithRulesFor($token, self::IN_BODY);
/* A start tag token with the tag name "head" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'head') {
/* Insert an HTML element for the token. */
$element = $this->insertElement($token);
/* Set the head element pointer to this new element node. */
$this->head_pointer = $element;
/* Change the insertion mode to "in head". */
$this->mode = self::IN_HEAD;
/* An end tag whose tag name is one of: "head", "body", "html", "br" */
} elseif(
$token['type'] === HTML5_Tokenizer::ENDTAG && (
$token['name'] === 'head' || $token['name'] === 'body' ||
$token['name'] === 'html' || $token['name'] === 'br'
)) {
/* Act as if a start tag token with the tag name "head" and no
* attributes had been seen, then reprocess the current token. */
$this->emitToken(array(
'name' => 'head',
'type' => HTML5_Tokenizer::STARTTAG,
'attr' => array()
));
$this->emitToken($token);
/* Any other end tag */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG) {
/* Parse error. Ignore the token. */
$this->ignored = true;
} else {
/* Act as if a start tag token with the tag name "head" and no
* attributes had been seen, then reprocess the current token.
* Note: This will result in an empty head element being
* generated, with the current token being reprocessed in the
* "after head" insertion mode. */
$this->emitToken(array(
'name' => 'head',
'type' => HTML5_Tokenizer::STARTTAG,
'attr' => array()
));
$this->emitToken($token);
}
break;
case self::IN_HEAD:
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
or U+0020 SPACE. */
if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
/* Insert the character into the current node. */
$this->insertText($token['data']);
/* A comment token */
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
/* Append a Comment node to the current node with the data attribute
set to the data given in the comment token. */
$this->insertComment($token['data']);
/* A DOCTYPE token */
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
/* Parse error. Ignore the token. */
$this->ignored = true;
// parse error
/* A start tag whose tag name is "html" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
$token['name'] === 'html') {
$this->processWithRulesFor($token, self::IN_BODY);
/* A start tag whose tag name is one of: "base", "command", "link" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
($token['name'] === 'base' || $token['name'] === 'command' ||
$token['name'] === 'link')) {
/* Insert an HTML element for the token. Immediately pop the
* current node off the stack of open elements. */
$this->insertElement($token);
array_pop($this->stack);
// YYY: Acknowledge the token's self-closing flag, if it is set.
/* A start tag whose tag name is "meta" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'meta') {
/* Insert an HTML element for the token. Immediately pop the
* current node off the stack of open elements. */
$this->insertElement($token);
array_pop($this->stack);
// XERROR: Acknowledge the token's self-closing flag, if it is set.
// XENCODING: If the element has a charset attribute, and its value is a
// supported encoding, and the confidence is currently tentative,
// then change the encoding to the encoding given by the value of
// the charset attribute.
//
// Otherwise, if the element has a content attribute, and applying
// the algorithm for extracting an encoding from a Content-Type to
// its value returns a supported encoding encoding, and the
// confidence is currently tentative, then change the encoding to
// the encoding encoding.
/* A start tag with the tag name "title" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'title') {
$this->insertRCDATAElement($token);
/* A start tag whose tag name is "noscript", if the scripting flag is enabled, or
* A start tag whose tag name is one of: "noframes", "style" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
($token['name'] === 'noscript' || $token['name'] === 'noframes' || $token['name'] === 'style')) {
// XSCRIPT: Scripting flag not respected
$this->insertCDATAElement($token);
// XSCRIPT: Scripting flag disable not implemented
/* A start tag with the tag name "script" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'script') {
/* 1. Create an element for the token in the HTML namespace. */
$node = $this->insertElement($token, false);
/* 2. Mark the element as being "parser-inserted" */
// Uhhh... XSCRIPT
/* 3. If the parser was originally created for the HTML
* fragment parsing algorithm, then mark the script element as
* "already executed". (fragment case) */
// ditto... XSCRIPT
/* 4. Append the new element to the current node and push it onto
* the stack of open elements. */
end($this->stack)->appendChild($node);
$this->stack[] = $node;
// I guess we could squash these together
/* 6. Let the original insertion mode be the current insertion mode. */
$this->original_mode = $this->mode;
/* 7. Switch the insertion mode to "in CDATA/RCDATA" */
$this->mode = self::IN_CDATA_RCDATA;
/* 5. Switch the tokeniser's content model flag to the CDATA state. */
$this->content_model = HTML5_Tokenizer::CDATA;
/* An end tag with the tag name "head" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'head') {
/* Pop the current node (which will be the head element) off the stack of open elements. */
array_pop($this->stack);
/* Change the insertion mode to "after head". */
$this->mode = self::AFTER_HEAD;
// Slight logic inversion here to minimize duplication
/* A start tag with the tag name "head". */
/* An end tag whose tag name is not one of: "body", "html", "br" */
} elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'head') ||
($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] !== 'html' &&
$token['name'] !== 'body' && $token['name'] !== 'br')) {
// Parse error. Ignore the token.
$this->ignored = true;
/* Anything else */
} else {
/* Act as if an end tag token with the tag name "head" had been
* seen, and reprocess the current token. */
$this->emitToken(array(
'name' => 'head',
'type' => HTML5_Tokenizer::ENDTAG
));
/* Then, reprocess the current token. */
$this->emitToken($token);
}
break;
case self::IN_HEAD_NOSCRIPT:
if ($token['type'] === HTML5_Tokenizer::DOCTYPE) {
// parse error
} elseif ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
$this->processWithRulesFor($token, self::IN_BODY);
} elseif ($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'noscript') {
/* Pop the current node (which will be a noscript element) from the
* stack of open elements; the new current node will be a head
* element. */
array_pop($this->stack);
$this->mode = self::IN_HEAD;
} elseif (
($token['type'] === HTML5_Tokenizer::SPACECHARACTER) ||
($token['type'] === HTML5_Tokenizer::COMMENT) ||
($token['type'] === HTML5_Tokenizer::STARTTAG && (
$token['name'] === 'link' || $token['name'] === 'meta' ||
$token['name'] === 'noframes' || $token['name'] === 'style'))) {
$this->processWithRulesFor($token, self::IN_HEAD);
// inverted logic
} elseif (
($token['type'] === HTML5_Tokenizer::STARTTAG && (
$token['name'] === 'head' || $token['name'] === 'noscript')) ||
($token['type'] === HTML5_Tokenizer::ENDTAG &&
$token['name'] !== 'br')) {
// parse error
} else {
// parse error
$this->emitToken(array(
'type' => HTML5_Tokenizer::ENDTAG,
'name' => 'noscript',
));
$this->emitToken($token);
}
break;
case self::AFTER_HEAD:
/* Handle the token as follows: */
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
or U+0020 SPACE */
if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
/* Append the character to the current node. */
$this->insertText($token['data']);
/* A comment token */
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
/* Append a Comment node to the current node with the data attribute
set to the data given in the comment token. */
$this->insertComment($token['data']);
} elseif ($token['type'] === HTML5_Tokenizer::DOCTYPE) {
// parse error
} elseif ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
$this->processWithRulesFor($token, self::IN_BODY);
/* A start tag token with the tag name "body" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'body') {
$this->insertElement($token);
/* Set the frameset-ok flag to "not ok". */
$this->flag_frameset_ok = false;
/* Change the insertion mode to "in body". */
$this->mode = self::IN_BODY;
/* A start tag token with the tag name "frameset" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'frameset') {
/* Insert a frameset element for the token. */
$this->insertElement($token);
/* Change the insertion mode to "in frameset". */
$this->mode = self::IN_FRAMESET;
/* A start tag token whose tag name is one of: "base", "link", "meta",
"script", "style", "title" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
array('base', 'link', 'meta', 'noframes', 'script', 'style', 'title'))) {
// parse error
/* Push the node pointed to by the head element pointer onto the
* stack of open elements. */
$this->stack[] = $this->head_pointer;
$this->processWithRulesFor($token, self::IN_HEAD);
array_splice($this->stack, array_search($this->head_pointer, $this->stack, true), 1);
// inversion of specification
} elseif(
($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'head') ||
($token['type'] === HTML5_Tokenizer::ENDTAG &&
$token['name'] !== 'body' && $token['name'] !== 'html' &&
$token['name'] !== 'br')) {
// parse error
/* Anything else */
} else {
$this->emitToken(array(
'name' => 'body',
'type' => HTML5_Tokenizer::STARTTAG,
'attr' => array()
));
$this->flag_frameset_ok = true;
$this->emitToken($token);
}
break;
case self::IN_BODY:
/* Handle the token as follows: */
switch($token['type']) {
/* A character token */
case HTML5_Tokenizer::CHARACTER:
case HTML5_Tokenizer::SPACECHARACTER:
/* Reconstruct the active formatting elements, if any. */
$this->reconstructActiveFormattingElements();
/* Append the token's character to the current node. */
$this->insertText($token['data']);
/* If the token is not one of U+0009 CHARACTER TABULATION,
* U+000A LINE FEED (LF), U+000C FORM FEED (FF), or U+0020
* SPACE, then set the frameset-ok flag to "not ok". */
// i.e., if any of the characters is not whitespace
if (strlen($token['data']) !== strspn($token['data'], HTML5_Tokenizer::WHITESPACE)) {
$this->flag_frameset_ok = false;
}
break;
/* A comment token */
case HTML5_Tokenizer::COMMENT:
/* Append a Comment node to the current node with the data
attribute set to the data given in the comment token. */
$this->insertComment($token['data']);
break;
case HTML5_Tokenizer::DOCTYPE:
// parse error
break;
case HTML5_Tokenizer::EOF:
// parse error
break;
case HTML5_Tokenizer::STARTTAG:
switch($token['name']) {
case 'html':
// parse error
/* For each attribute on the token, check to see if the
* attribute is already present on the top element of the
* stack of open elements. If it is not, add the attribute
* and its corresponding value to that element. */
foreach($token['attr'] as $attr) {
if(!$this->stack[0]->hasAttribute($attr['name'])) {
$this->stack[0]->setAttribute($attr['name'], $attr['value']);
}
}
break;
case 'base': case 'command': case 'link': case 'meta': case 'noframes':
case 'script': case 'style': case 'title':
/* Process the token as if the insertion mode had been "in
head". */
$this->processWithRulesFor($token, self::IN_HEAD);
break;
/* A start tag token with the tag name "body" */
case 'body':
/* Parse error. If the second element on the stack of open
elements is not a body element, or, if the stack of open
elements has only one node on it, then ignore the token.
(fragment case) */
if(count($this->stack) === 1 || $this->stack[1]->tagName !== 'body') {
$this->ignored = true;
// Ignore
/* Otherwise, for each attribute on the token, check to see
if the attribute is already present on the body element (the
second element) on the stack of open elements. If it is not,
add the attribute and its corresponding value to that
element. */
} else {
foreach($token['attr'] as $attr) {
if(!$this->stack[1]->hasAttribute($attr['name'])) {
$this->stack[1]->setAttribute($attr['name'], $attr['value']);
}
}
}
break;
case 'frameset':
// parse error
/* If the second element on the stack of open elements is
* not a body element, or, if the stack of open elements
* has only one node on it, then ignore the token.
* (fragment case) */
if(count($this->stack) === 1 || $this->stack[1]->tagName !== 'body') {
$this->ignored = true;
// Ignore
} elseif (!$this->flag_frameset_ok) {
$this->ignored = true;
// Ignore
} else {
/* 1. Remove the second element on the stack of open
* elements from its parent node, if it has one. */
if($this->stack[1]->parentNode) {
$this->stack[1]->parentNode->removeChild($this->stack[1]);
}
/* 2. Pop all the nodes from the bottom of the stack of
* open elements, from the current node up to the root
* html element. */
array_splice($this->stack, 1);
$this->insertElement($token);
$this->mode = self::IN_FRAMESET;
}
break;
// in spec, there is a diversion here
case 'address': case 'article': case 'aside': case 'blockquote':
case 'center': case 'datagrid': case 'details': case 'dir':
case 'div': case 'dl': case 'fieldset': case 'figure': case 'footer':
case 'header': case 'hgroup': case 'menu': case 'nav':
case 'ol': case 'p': case 'section': case 'ul':
/* If the stack of open elements has a p element in scope,
then act as if an end tag with the tag name p had been
seen. */
if($this->elementInScope('p')) {
$this->emitToken(array(
'name' => 'p',
'type' => HTML5_Tokenizer::ENDTAG
));
}
/* Insert an HTML element for the token. */
$this->insertElement($token);
break;
/* A start tag whose tag name is one of: "h1", "h2", "h3", "h4",
"h5", "h6" */
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
/* If the stack of open elements has a p element in scope,
then act as if an end tag with the tag name p had been seen. */
if($this->elementInScope('p')) {
$this->emitToken(array(
'name' => 'p',
'type' => HTML5_Tokenizer::ENDTAG
));
}
/* If the current node is an element whose tag name is one
* of "h1", "h2", "h3", "h4", "h5", or "h6", then this is a
* parse error; pop the current node off the stack of open
* elements. */
$peek = array_pop($this->stack);
if (in_array($peek->tagName, array("h1", "h2", "h3", "h4", "h5", "h6"))) {
// parse error
} else {
$this->stack[] = $peek;
}
/* Insert an HTML element for the token. */
$this->insertElement($token);
break;
case 'pre': case 'listing':
/* If the stack of open elements has a p element in scope,
then act as if an end tag with the tag name p had been seen. */
if($this->elementInScope('p')) {
$this->emitToken(array(
'name' => 'p',
'type' => HTML5_Tokenizer::ENDTAG
));
}
$this->insertElement($token);
/* If the next token is a U+000A LINE FEED (LF) character
* token, then ignore that token and move on to the next
* one. (Newlines at the start of pre blocks are ignored as
* an authoring convenience.) */
$this->ignore_lf_token = 2;
$this->flag_frameset_ok = false;
break;
/* A start tag whose tag name is "form" */
case 'form':
/* If the form element pointer is not null, ignore the
token with a parse error. */
if($this->form_pointer !== null) {
$this->ignored = true;
// Ignore.
/* Otherwise: */
} else {
/* If the stack of open elements has a p element in
scope, then act as if an end tag with the tag name p
had been seen. */
if($this->elementInScope('p')) {
$this->emitToken(array(
'name' => 'p',
'type' => HTML5_Tokenizer::ENDTAG
));
}
/* Insert an HTML element for the token, and set the
form element pointer to point to the element created. */
$element = $this->insertElement($token);
$this->form_pointer = $element;
}
break;
// condensed specification
case 'li': case 'dc': case 'dd': case 'ds': case 'dt':
/* 1. Set the frameset-ok flag to "not ok". */
$this->flag_frameset_ok = false;
$stack_length = count($this->stack) - 1;
for($n = $stack_length; 0 <= $n; $n--) {
/* 2. Initialise node to be the current node (the
bottommost node of the stack). */
$stop = false;
$node = $this->stack[$n];
$cat = $this->getElementCategory($node);
// for case 'li':
/* 3. If node is an li element, then act as if an end
* tag with the tag name "li" had been seen, then jump
* to the last step. */
// for case 'dc': case 'dd': case 'ds': case 'dt':
/* If node is a dc, dd, ds or dt element, then act as if an end
* tag with the same tag name as node had been seen, then
* jump to the last step. */
if(($token['name'] === 'li' && $node->tagName === 'li') ||
($token['name'] !== 'li' && ($node->tagName == 'dc' || $node->tagName === 'dd' || $node->tagName == 'ds' || $node->tagName === 'dt'))) { // limited conditional
$this->emitToken(array(
'type' => HTML5_Tokenizer::ENDTAG,
'name' => $node->tagName,
));
break;
}
/* 4. If node is not in the formatting category, and is
not in the phrasing category, and is not an address,
div or p element, then stop this algorithm. */
if($cat !== self::FORMATTING && $cat !== self::PHRASING &&
$node->tagName !== 'address' && $node->tagName !== 'div' &&
$node->tagName !== 'p') {
break;
}
/* 5. Otherwise, set node to the previous entry in the
* stack of open elements and return to step 2. */
}
/* 6. This is the last step. */
/* If the stack of open elements has a p element in scope,
then act as if an end tag with the tag name p had been
seen. */
if($this->elementInScope('p')) {
$this->emitToken(array(
'name' => 'p',
'type' => HTML5_Tokenizer::ENDTAG
));
}
/* Finally, insert an HTML element with the same tag
name as the token's. */
$this->insertElement($token);
break;
/* A start tag token whose tag name is "plaintext" */
case 'plaintext':
/* If the stack of open elements has a p element in scope,
then act as if an end tag with the tag name p had been
seen. */
if($this->elementInScope('p')) {
$this->emitToken(array(
'name' => 'p',
'type' => HTML5_Tokenizer::ENDTAG
));
}
/* Insert an HTML element for the token. */
$this->insertElement($token);
$this->content_model = HTML5_Tokenizer::PLAINTEXT;
break;
// more diversions
/* A start tag whose tag name is "a" */
case 'a':
/* If the list of active formatting elements contains
an element whose tag name is "a" between the end of the
list and the last marker on the list (or the start of
the list if there is no marker on the list), then this
is a parse error; act as if an end tag with the tag name
"a" had been seen, then remove that element from the list
of active formatting elements and the stack of open
elements if the end tag didn't already remove it (it
might not have if the element is not in table scope). */
$leng = count($this->a_formatting);
for($n = $leng - 1; $n >= 0; $n--) {
if($this->a_formatting[$n] === self::MARKER) {
break;
} elseif($this->a_formatting[$n]->tagName === 'a') {
$a = $this->a_formatting[$n];
$this->emitToken(array(
'name' => 'a',
'type' => HTML5_Tokenizer::ENDTAG
));
if (in_array($a, $this->a_formatting)) {
$a_i = array_search($a, $this->a_formatting, true);
if($a_i !== false) array_splice($this->a_formatting, $a_i, 1);
}
if (in_array($a, $this->stack)) {
$a_i = array_search($a, $this->stack, true);
if ($a_i !== false) array_splice($this->stack, $a_i, 1);
}
break;
}
}
/* Reconstruct the active formatting elements, if any. */
$this->reconstructActiveFormattingElements();
/* Insert an HTML element for the token. */
$el = $this->insertElement($token);
/* Add that element to the list of active formatting
elements. */
$this->a_formatting[] = $el;
break;
case 'b': case 'big': case 'code': case 'em': case 'font': case 'i':
case 's': case 'small': case 'strike':
case 'strong': case 'tt': case 'u':
/* Reconstruct the active formatting elements, if any. */
$this->reconstructActiveFormattingElements();
/* Insert an HTML element for the token. */
$el = $this->insertElement($token);
/* Add that element to the list of active formatting
elements. */
$this->a_formatting[] = $el;
break;
case 'nobr':
/* Reconstruct the active formatting elements, if any. */
$this->reconstructActiveFormattingElements();
/* If the stack of open elements has a nobr element in
* scope, then this is a parse error; act as if an end tag
* with the tag name "nobr" had been seen, then once again
* reconstruct the active formatting elements, if any. */
if ($this->elementInScope('nobr')) {
$this->emitToken(array(
'name' => 'nobr',
'type' => HTML5_Tokenizer::ENDTAG,
));
$this->reconstructActiveFormattingElements();
}
/* Insert an HTML element for the token. */
$el = $this->insertElement($token);
/* Add that element to the list of active formatting
elements. */
$this->a_formatting[] = $el;
break;
// another diversion
/* A start tag token whose tag name is "button" */
case 'button':
/* If the stack of open elements has a button element in scope,
then this is a parse error; act as if an end tag with the tag
name "button" had been seen, then reprocess the token. (We don't
do that. Unnecessary.) (I hope you're right! -- ezyang) */
if($this->elementInScope('button')) {
$this->emitToken(array(
'name' => 'button',
'type' => HTML5_Tokenizer::ENDTAG
));
}
/* Reconstruct the active formatting elements, if any. */
$this->reconstructActiveFormattingElements();
/* Insert an HTML element for the token. */
$this->insertElement($token);
/* Insert a marker at the end of the list of active
formatting elements. */
$this->a_formatting[] = self::MARKER;
$this->flag_frameset_ok = false;
break;
case 'applet': case 'marquee': case 'object':
/* Reconstruct the active formatting elements, if any. */
$this->reconstructActiveFormattingElements();
/* Insert an HTML element for the token. */
$this->insertElement($token);
/* Insert a marker at the end of the list of active
formatting elements. */
$this->a_formatting[] = self::MARKER;
$this->flag_frameset_ok = false;
break;
// spec diversion
/* A start tag whose tag name is "table" */
case 'table':
/* If the Document is not set to quirks mode, and the
* stack of open elements has a p element in scope, then
* act as if an end tag with the tag name "p" had been
* seen. */
if($this->quirks_mode !== self::QUIRKS_MODE &&
$this->elementInScope('p')) {
$this->emitToken(array(
'name' => 'p',
'type' => HTML5_Tokenizer::ENDTAG
));
}
/* Insert an HTML element for the token. */
$this->insertElement($token);
$this->flag_frameset_ok = false;
/* Change the insertion mode to "in table". */
$this->mode = self::IN_TABLE;
break;
/* A start tag whose tag name is one of: "area", "basefont",
"bgsound", "br", "embed", "img", "param", "spacer", "wbr" */
case 'area': case 'basefont': case 'bgsound': case 'br':
case 'embed': case 'img': case 'input': case 'keygen': case 'spacer':
case 'wbr':
/* Reconstruct the active formatting elements, if any. */
$this->reconstructActiveFormattingElements();
/* Insert an HTML element for the token. */
$this->insertElement($token);
/* Immediately pop the current node off the stack of open elements. */
array_pop($this->stack);
// YYY: Acknowledge the token's self-closing flag, if it is set.
$this->flag_frameset_ok = false;
break;
case 'param': case 'source':
/* Insert an HTML element for the token. */
$this->insertElement($token);
/* Immediately pop the current node off the stack of open elements. */
array_pop($this->stack);
// YYY: Acknowledge the token's self-closing flag, if it is set.
break;
/* A start tag whose tag name is "hr" */
case 'hr':
/* If the stack of open elements has a p element in scope,
then act as if an end tag with the tag name p had been seen. */
if($this->elementInScope('p')) {
$this->emitToken(array(
'name' => 'p',
'type' => HTML5_Tokenizer::ENDTAG
));
}
/* Insert an HTML element for the token. */
$this->insertElement($token);
/* Immediately pop the current node off the stack of open elements. */
array_pop($this->stack);
// YYY: Acknowledge the token's self-closing flag, if it is set.
$this->flag_frameset_ok = false;
break;
/* A start tag whose tag name is "image" */
case 'image':
/* Parse error. Change the token's tag name to "img" and
reprocess it. (Don't ask.) */
$token['name'] = 'img';
$this->emitToken($token);
break;
/* A start tag whose tag name is "isindex" */
case 'isindex':
/* Parse error. */
/* If the form element pointer is not null,
then ignore the token. */
if($this->form_pointer === null) {
/* Act as if a start tag token with the tag name "form" had
been seen. */
/* If the token has an attribute called "action", set
* the action attribute on the resulting form
* element to the value of the "action" attribute of
* the token. */
$attr = array();
$action = $this->getAttr($token, 'action');
if ($action !== false) {
$attr[] = array('name' => 'action', 'value' => $action);
}
$this->emitToken(array(
'name' => 'form',
'type' => HTML5_Tokenizer::STARTTAG,
'attr' => $attr
));
/* Act as if a start tag token with the tag name "hr" had
been seen. */
$this->emitToken(array(
'name' => 'hr',
'type' => HTML5_Tokenizer::STARTTAG,
'attr' => array()
));
/* Act as if a start tag token with the tag name "label"
had been seen. */
$this->emitToken(array(
'name' => 'label',
'type' => HTML5_Tokenizer::STARTTAG,
'attr' => array()
));
/* Act as if a stream of character tokens had been seen. */
$prompt = $this->getAttr($token, 'prompt');
if ($prompt === false) {
$prompt = 'This is a searchable index. '.
'Insert your search keywords here: ';
}
$this->emitToken(array(
'data' => $prompt,
'type' => HTML5_Tokenizer::CHARACTER,
));
/* Act as if a start tag token with the tag name "input"
had been seen, with all the attributes from the "isindex"
token, except with the "name" attribute set to the value
"isindex" (ignoring any explicit "name" attribute). */
$attr = array();
foreach ($token['attr'] as $keypair) {
if ($keypair['name'] === 'name' || $keypair['name'] === 'action' ||
$keypair['name'] === 'prompt') continue;
$attr[] = $keypair;
}
$attr[] = array('name' => 'name', 'value' => 'isindex');
$this->emitToken(array(
'name' => 'input',
'type' => HTML5_Tokenizer::STARTTAG,
'attr' => $attr
));
/* Act as if an end tag token with the tag name "label"
had been seen. */
$this->emitToken(array(
'name' => 'label',
'type' => HTML5_Tokenizer::ENDTAG
));
/* Act as if a start tag token with the tag name "hr" had
been seen. */
$this->emitToken(array(
'name' => 'hr',
'type' => HTML5_Tokenizer::STARTTAG
));
/* Act as if an end tag token with the tag name "form" had
been seen. */
$this->emitToken(array(
'name' => 'form',
'type' => HTML5_Tokenizer::ENDTAG
));
} else {
$this->ignored = true;
}
break;
/* A start tag whose tag name is "textarea" */
case 'textarea':
$this->insertElement($token);
/* If the next token is a U+000A LINE FEED (LF)
* character token, then ignore that token and move on to
* the next one. (Newlines at the start of textarea
* elements are ignored as an authoring convenience.)
* need flag, see also <pre> */
$this->ignore_lf_token = 2;
$this->original_mode = $this->mode;
$this->flag_frameset_ok = false;
$this->mode = self::IN_CDATA_RCDATA;
/* Switch the tokeniser's content model flag to the
RCDATA state. */
$this->content_model = HTML5_Tokenizer::RCDATA;
break;
/* A start tag token whose tag name is "xmp" */
case 'xmp':
/* If the stack of open elements has a p element in
scope, then act as if an end tag with the tag name
"p" has been seen. */
if ($this->elementInScope('p')) {
$this->emitToken(array(
'name' => 'p',
'type' => HTML5_Tokenizer::ENDTAG
));
}
/* Reconstruct the active formatting elements, if any. */
$this->reconstructActiveFormattingElements();
$this->flag_frameset_ok = false;
$this->insertCDATAElement($token);
break;
case 'iframe':
$this->flag_frameset_ok = false;
$this->insertCDATAElement($token);
break;
case 'noembed': case 'noscript':
// XSCRIPT: should check scripting flag
$this->insertCDATAElement($token);
break;
/* A start tag whose tag name is "select" */
case 'select':
/* Reconstruct the active formatting elements, if any. */
$this->reconstructActiveFormattingElements();
/* Insert an HTML element for the token. */
$this->insertElement($token);
$this->flag_frameset_ok = false;
/* If the insertion mode is one of in table", "in caption",
* "in column group", "in table body", "in row", or "in
* cell", then switch the insertion mode to "in select in
* table". Otherwise, switch the insertion mode to "in
* select". */
if (
$this->mode === self::IN_TABLE || $this->mode === self::IN_CAPTION ||
$this->mode === self::IN_COLUMN_GROUP || $this->mode ==+self::IN_TABLE_BODY ||
$this->mode === self::IN_ROW || $this->mode === self::IN_CELL
) {
$this->mode = self::IN_SELECT_IN_TABLE;
} else {
$this->mode = self::IN_SELECT;
}
break;
case 'option': case 'optgroup':
if ($this->elementInScope('option')) {
$this->emitToken(array(
'name' => 'option',
'type' => HTML5_Tokenizer::ENDTAG,
));
}
$this->reconstructActiveFormattingElements();
$this->insertElement($token);
break;
case 'rp': case 'rt':
/* If the stack of open elements has a ruby element in scope, then generate
* implied end tags. If the current node is not then a ruby element, this is
* a parse error; pop all the nodes from the current node up to the node
* immediately before the bottommost ruby element on the stack of open elements.
*/
if ($this->elementInScope('ruby')) {
$this->generateImpliedEndTags();
}
$peek = false;
do {
if ($peek) {
// parse error
}
$peek = array_pop($this->stack);
} while ($peek->tagName !== 'ruby');
$this->stack[] = $peek; // we popped one too many
$this->insertElement($token);
break;
// spec diversion
case 'math':
$this->reconstructActiveFormattingElements();
$token = $this->adjustMathMLAttributes($token);
$token = $this->adjustForeignAttributes($token);
$this->insertForeignElement($token, self::NS_MATHML);
if (isset($token['self-closing'])) {
// XERROR: acknowledge the token's self-closing flag
array_pop($this->stack);
}
if ($this->mode !== self::IN_FOREIGN_CONTENT) {
$this->secondary_mode = $this->mode;
$this->mode = self::IN_FOREIGN_CONTENT;
}
break;
case 'svg':
$this->reconstructActiveFormattingElements();
$token = $this->adjustSVGAttributes($token);
$token = $this->adjustForeignAttributes($token);
$this->insertForeignElement($token, self::NS_SVG);
if (isset($token['self-closing'])) {
// XERROR: acknowledge the token's self-closing flag
array_pop($this->stack);
}
if ($this->mode !== self::IN_FOREIGN_CONTENT) {
$this->secondary_mode = $this->mode;
$this->mode = self::IN_FOREIGN_CONTENT;
}
break;
case 'caption': case 'col': case 'colgroup': case 'frame': case 'head':
case 'tbody': case 'td': case 'tfoot': case 'th': case 'thead': case 'tr':
// parse error
break;
/* A start tag token not covered by the previous entries */
default:
/* Reconstruct the active formatting elements, if any. */
$this->reconstructActiveFormattingElements();
$this->insertElement($token);
/* This element will be a phrasing element. */
break;
}
break;
case HTML5_Tokenizer::ENDTAG:
switch($token['name']) {
/* An end tag with the tag name "body" */
case 'body':
/* If the stack of open elements does not have a body
* element in scope, this is a parse error; ignore the
* token. */
if(!$this->elementInScope('body')) {
$this->ignored = true;
/* Otherwise, if there is a node in the stack of open
* elements that is not either a dc element, a dd element,
* a ds element, a dt element, an li element, an optgroup
* element, an option element, a p element, an rp element,
* an rt element, a tbody element, a td element, a tfoot
* element, a th element, a thead element, a tr element,
* the body element, or the html element, then this is a
* parse error.
*/
} else {
// XERROR: implement this check for parse error
}
/* Change the insertion mode to "after body". */
$this->mode = self::AFTER_BODY;
break;
/* An end tag with the tag name "html" */
case 'html':
/* Act as if an end tag with tag name "body" had been seen,
then, if that token wasn't ignored, reprocess the current
token. */
$this->emitToken(array(
'name' => 'body',
'type' => HTML5_Tokenizer::ENDTAG
));
if (!$this->ignored) $this->emitToken($token);
break;
case 'address': case 'article': case 'aside': case 'blockquote':
case 'center': case 'datagrid': case 'details': case 'dir':
case 'div': case 'dl': case 'fieldset': case 'footer':
case 'header': case 'hgroup': case 'listing': case 'menu':
case 'nav': case 'ol': case 'pre': case 'section': case 'ul':
/* If the stack of open elements has an element in scope
with the same tag name as that of the token, then generate
implied end tags. */
if($this->elementInScope($token['name'])) {
$this->generateImpliedEndTags();
/* Now, if the current node is not an element with
the same tag name as that of the token, then this
is a parse error. */
// XERROR: implement parse error logic
/* If the stack of open elements has an element in
scope with the same tag name as that of the token,
then pop elements from this stack until an element
with that tag name has been popped from the stack. */
do {
$node = array_pop($this->stack);
} while ($node->tagName !== $token['name']);
} else {
// parse error
}
break;
/* An end tag whose tag name is "form" */
case 'form':
/* Let node be the element that the form element pointer is set to. */
$node = $this->form_pointer;
/* Set the form element pointer to null. */
$this->form_pointer = null;
/* If node is null or the stack of open elements does not
* have node in scope, then this is a parse error; ignore the token. */
if ($node === null || !in_array($node, $this->stack)) {
// parse error
$this->ignored = true;
} else {
/* 1. Generate implied end tags. */
$this->generateImpliedEndTags();
/* 2. If the current node is not node, then this is a parse error. */
if (end($this->stack) !== $node) {
// parse error
}
/* 3. Remove node from the stack of open elements. */
array_splice($this->stack, array_search($node, $this->stack, true), 1);
}
break;
/* An end tag whose tag name is "p" */
case 'p':
/* If the stack of open elements has a p element in scope,
then generate implied end tags, except for p elements. */
if($this->elementInScope('p')) {
/* Generate implied end tags, except for elements with
* the same tag name as the token. */
$this->generateImpliedEndTags(array('p'));
/* If the current node is not a p element, then this is
a parse error. */
// XERROR: implement
/* Pop elements from the stack of open elements until
* an element with the same tag name as the token has
* been popped from the stack. */
do {
$node = array_pop($this->stack);
} while ($node->tagName !== 'p');
} else {
// parse error
$this->emitToken(array(
'name' => 'p',
'type' => HTML5_Tokenizer::STARTTAG,
));
$this->emitToken($token);
}
break;
/* An end tag whose tag name is "li" */
case 'li':
/* If the stack of open elements does not have an element
* in list item scope with the same tag name as that of the
* token, then this is a parse error; ignore the token. */
if ($this->elementInScope($token['name'], self::SCOPE_LISTITEM)) {
/* Generate implied end tags, except for elements with the
* same tag name as the token. */
$this->generateImpliedEndTags(array($token['name']));
/* If the current node is not an element with the same tag
* name as that of the token, then this is a parse error. */
// XERROR: parse error
/* Pop elements from the stack of open elements until an
* element with the same tag name as the token has been
* popped from the stack. */
do {
$node = array_pop($this->stack);
} while ($node->tagName !== $token['name']);
} else {
// XERROR: parse error
}
break;
/* An end tag whose tag name is "dc", "dd", "ds", "dt" */
case 'dc': case 'dd': case 'ds': case 'dt':
if($this->elementInScope($token['name'])) {
$this->generateImpliedEndTags(array($token['name']));
/* If the current node is not an element with the same
tag name as the token, then this is a parse error. */
// XERROR: implement parse error
/* Pop elements from the stack of open elements until
* an element with the same tag name as the token has
* been popped from the stack. */
do {
$node = array_pop($this->stack);
} while ($node->tagName !== $token['name']);
} else {
// XERROR: parse error
}
break;
/* An end tag whose tag name is one of: "h1", "h2", "h3", "h4",
"h5", "h6" */
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
$elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6');
/* If the stack of open elements has in scope an element whose
tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then
generate implied end tags. */
if($this->elementInScope($elements)) {
$this->generateImpliedEndTags();
/* Now, if the current node is not an element with the same
tag name as that of the token, then this is a parse error. */
// XERROR: implement parse error
/* If the stack of open elements has in scope an element
whose tag name is one of "h1", "h2", "h3", "h4", "h5", or
"h6", then pop elements from the stack until an element
with one of those tag names has been popped from the stack. */
do {
$node = array_pop($this->stack);
} while (!in_array($node->tagName, $elements));
} else {
// parse error
}
break;
/* An end tag whose tag name is one of: "a", "b", "big", "em",
"font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */
case 'a': case 'b': case 'big': case 'code': case 'em': case 'font':
case 'i': case 'nobr': case 's': case 'small': case 'strike':
case 'strong': case 'tt': case 'u':
// XERROR: generally speaking this needs parse error logic
/* 1. Let the formatting element be the last element in
the list of active formatting elements that:
* is between the end of the list and the last scope
marker in the list, if any, or the start of the list
otherwise, and
* has the same tag name as the token.
*/
while(true) {
for($a = count($this->a_formatting) - 1; $a >= 0; $a--) {
if($this->a_formatting[$a] === self::MARKER) {
break;
} elseif($this->a_formatting[$a]->tagName === $token['name']) {
$formatting_element = $this->a_formatting[$a];
$in_stack = in_array($formatting_element, $this->stack, true);
$fe_af_pos = $a;
break;
}
}
/* If there is no such node, or, if that node is
also in the stack of open elements but the element
is not in scope, then this is a parse error. Abort
these steps. The token is ignored. */
if(!isset($formatting_element) || ($in_stack &&
!$this->elementInScope($token['name']))) {
$this->ignored = true;
break;
/* Otherwise, if there is such a node, but that node
is not in the stack of open elements, then this is a
parse error; remove the element from the list, and
abort these steps. */
} elseif(isset($formatting_element) && !$in_stack) {
unset($this->a_formatting[$fe_af_pos]);
$this->a_formatting = array_merge($this->a_formatting);
break;
}
/* Otherwise, there is a formatting element and that
* element is in the stack and is in scope. If the
* element is not the current node, this is a parse
* error. In any case, proceed with the algorithm as
* written in the following steps. */
// XERROR: implement me
/* 2. Let the furthest block be the topmost node in the
stack of open elements that is lower in the stack
than the formatting element, and is not an element in
the phrasing or formatting categories. There might
not be one. */
$fe_s_pos = array_search($formatting_element, $this->stack, true);
$length = count($this->stack);
for($s = $fe_s_pos + 1; $s < $length; $s++) {
$category = $this->getElementCategory($this->stack[$s]);
if($category !== self::PHRASING && $category !== self::FORMATTING) {
$furthest_block = $this->stack[$s];
break;
}
}
/* 3. If there is no furthest block, then the UA must
skip the subsequent steps and instead just pop all
the nodes from the bottom of the stack of open
elements, from the current node up to the formatting
element, and remove the formatting element from the
list of active formatting elements. */
if(!isset($furthest_block)) {
for($n = $length - 1; $n >= $fe_s_pos; $n--) {
array_pop($this->stack);
}
unset($this->a_formatting[$fe_af_pos]);
$this->a_formatting = array_merge($this->a_formatting);
break;
}
/* 4. Let the common ancestor be the element
immediately above the formatting element in the stack
of open elements. */
$common_ancestor = $this->stack[$fe_s_pos - 1];
/* 5. Let a bookmark note the position of the
formatting element in the list of active formatting
elements relative to the elements on either side
of it in the list. */
$bookmark = $fe_af_pos;
/* 6. Let node and last node be the furthest block.
Follow these steps: */
$node = $furthest_block;
$last_node = $furthest_block;
while(true) {
for($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) {
/* 6.1 Let node be the element immediately
prior to node in the stack of open elements. */
$node = $this->stack[$n];
/* 6.2 If node is not in the list of active
formatting elements, then remove node from
the stack of open elements and then go back
to step 1. */
if(!in_array($node, $this->a_formatting, true)) {
array_splice($this->stack, $n, 1);
} else {
break;
}
}
/* 6.3 Otherwise, if node is the formatting
element, then go to the next step in the overall
algorithm. */
if($node === $formatting_element) {
break;
/* 6.4 Otherwise, if last node is the furthest
block, then move the aforementioned bookmark to
be immediately after the node in the list of
active formatting elements. */
} elseif($last_node === $furthest_block) {
$bookmark = array_search($node, $this->a_formatting, true) + 1;
}
/* 6.5 Create an element for the token for which
* the element node was created, replace the entry
* for node in the list of active formatting
* elements with an entry for the new element,
* replace the entry for node in the stack of open
* elements with an entry for the new element, and
* let node be the new element. */
// we don't know what the token is anymore
// XDOM
$clone = $node->cloneNode();
$a_pos = array_search($node, $this->a_formatting, true);
$s_pos = array_search($node, $this->stack, true);
$this->a_formatting[$a_pos] = $clone;
$this->stack[$s_pos] = $clone;
$node = $clone;
/* 6.6 Insert last node into node, first removing
it from its previous parent node if any. */
// XDOM
if($last_node->parentNode !== null) {
$last_node->parentNode->removeChild($last_node);
}
// XDOM
$node->appendChild($last_node);
/* 6.7 Let last node be node. */
$last_node = $node;
/* 6.8 Return to step 1 of this inner set of steps. */
}
/* 7. If the common ancestor node is a table, tbody,
* tfoot, thead, or tr element, then, foster parent
* whatever last node ended up being in the previous
* step, first removing it from its previous parent
* node if any. */
// XDOM
if ($last_node->parentNode) { // common step
$last_node->parentNode->removeChild($last_node);
}
if (in_array($common_ancestor->tagName, array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
$this->fosterParent($last_node);
/* Otherwise, append whatever last node ended up being
* in the previous step to the common ancestor node,
* first removing it from its previous parent node if
* any. */
} else {
// XDOM
$common_ancestor->appendChild($last_node);
}
/* 8. Create an element for the token for which the
* formatting element was created. */
// XDOM
$clone = $formatting_element->cloneNode();
/* 9. Take all of the child nodes of the furthest
block and append them to the element created in the
last step. */
// XDOM
while($furthest_block->hasChildNodes()) {
$child = $furthest_block->firstChild;
$furthest_block->removeChild($child);
$clone->appendChild($child);
}
/* 10. Append that clone to the furthest block. */
// XDOM
$furthest_block->appendChild($clone);
/* 11. Remove the formatting element from the list
of active formatting elements, and insert the new element
into the list of active formatting elements at the
position of the aforementioned bookmark. */
$fe_af_pos = array_search($formatting_element, $this->a_formatting, true);
array_splice($this->a_formatting, $fe_af_pos, 1);
$af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1);
$af_part2 = array_slice($this->a_formatting, $bookmark);
$this->a_formatting = array_merge($af_part1, array($clone), $af_part2);
/* 12. Remove the formatting element from the stack
of open elements, and insert the new element into the stack
of open elements immediately below the position of the
furthest block in that stack. */
$fe_s_pos = array_search($formatting_element, $this->stack, true);
array_splice($this->stack, $fe_s_pos, 1);
$fb_s_pos = array_search($furthest_block, $this->stack, true);
$s_part1 = array_slice($this->stack, 0, $fb_s_pos + 1);
$s_part2 = array_slice($this->stack, $fb_s_pos + 1);
$this->stack = array_merge($s_part1, array($clone), $s_part2);
/* 13. Jump back to step 1 in this series of steps. */
unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block);
}
break;
case 'applet': case 'button': case 'marquee': case 'object':
/* If the stack of open elements has an element in scope whose
tag name matches the tag name of the token, then generate implied
tags. */
if($this->elementInScope($token['name'])) {
$this->generateImpliedEndTags();
/* Now, if the current node is not an element with the same
tag name as the token, then this is a parse error. */
// XERROR: implement logic
/* Pop elements from the stack of open elements until
* an element with the same tag name as the token has
* been popped from the stack. */
do {
$node = array_pop($this->stack);
} while ($node->tagName !== $token['name']);
/* Clear the list of active formatting elements up to the
* last marker. */
$keys = array_keys($this->a_formatting, self::MARKER, true);
$marker = end($keys);
for($n = count($this->a_formatting) - 1; $n > $marker; $n--) {
array_pop($this->a_formatting);
}
} else {
// parse error
}
break;
case 'br':
// Parse error
$this->emitToken(array(
'name' => 'br',
'type' => HTML5_Tokenizer::STARTTAG,
));
break;
/* An end tag token not covered by the previous entries */
default:
for($n = count($this->stack) - 1; $n >= 0; $n--) {
/* Initialise node to be the current node (the bottommost
node of the stack). */
$node = $this->stack[$n];
/* If node has the same tag name as the end tag token,
then: */
if($token['name'] === $node->tagName) {
/* Generate implied end tags. */
$this->generateImpliedEndTags();
/* If the tag name of the end tag token does not
match the tag name of the current node, this is a
parse error. */
// XERROR: implement this
/* Pop all the nodes from the current node up to
node, including node, then stop these steps. */
// XSKETCHY
do {
$pop = array_pop($this->stack);
} while ($pop !== $node);
break;
} else {
$category = $this->getElementCategory($node);
if($category !== self::FORMATTING && $category !== self::PHRASING) {
/* Otherwise, if node is in neither the formatting
category nor the phrasing category, then this is a
parse error. Stop this algorithm. The end tag token
is ignored. */
$this->ignored = true;
break;
// parse error
}
}
/* Set node to the previous entry in the stack of open elements. Loop. */
}
break;
}
break;
}
break;
case self::IN_CDATA_RCDATA:
if (
$token['type'] === HTML5_Tokenizer::CHARACTER ||
$token['type'] === HTML5_Tokenizer::SPACECHARACTER
) {
$this->insertText($token['data']);
} elseif ($token['type'] === HTML5_Tokenizer::EOF) {
// parse error
/* If the current node is a script element, mark the script
* element as "already executed". */
// probably not necessary
array_pop($this->stack);
$this->mode = $this->original_mode;
$this->emitToken($token);
} elseif ($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'script') {
array_pop($this->stack);
$this->mode = $this->original_mode;
// we're ignoring all of the execution stuff
} elseif ($token['type'] === HTML5_Tokenizer::ENDTAG) {
array_pop($this->stack);
$this->mode = $this->original_mode;
}
break;
case self::IN_TABLE:
$clear = array('html', 'table');
/* A character token */
if ($token['type'] === HTML5_Tokenizer::CHARACTER ||
$token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
/* Let the pending table character tokens
* be an empty list of tokens. */
$this->pendingTableCharacters = "";
$this->pendingTableCharactersDirty = false;
/* Let the original insertion mode be the current
* insertion mode. */
$this->original_mode = $this->mode;
/* Switch the insertion mode to
* "in table text" and
* reprocess the token. */
$this->mode = self::IN_TABLE_TEXT;
$this->emitToken($token);
/* A comment token */
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
/* Append a Comment node to the current node with the data
attribute set to the data given in the comment token. */
$this->insertComment($token['data']);
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
// parse error
/* A start tag whose tag name is "caption" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
$token['name'] === 'caption') {
/* Clear the stack back to a table context. */
$this->clearStackToTableContext($clear);
/* Insert a marker at the end of the list of active
formatting elements. */
$this->a_formatting[] = self::MARKER;
/* Insert an HTML element for the token, then switch the
insertion mode to "in caption". */
$this->insertElement($token);
$this->mode = self::IN_CAPTION;
/* A start tag whose tag name is "colgroup" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
$token['name'] === 'colgroup') {
/* Clear the stack back to a table context. */
$this->clearStackToTableContext($clear);
/* Insert an HTML element for the token, then switch the
insertion mode to "in column group". */
$this->insertElement($token);
$this->mode = self::IN_COLUMN_GROUP;
/* A start tag whose tag name is "col" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
$token['name'] === 'col') {
$this->emitToken(array(
'name' => 'colgroup',
'type' => HTML5_Tokenizer::STARTTAG,
'attr' => array()
));
$this->emitToken($token);
/* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
array('tbody', 'tfoot', 'thead'))) {
/* Clear the stack back to a table context. */
$this->clearStackToTableContext($clear);
/* Insert an HTML element for the token, then switch the insertion
mode to "in table body". */
$this->insertElement($token);
$this->mode = self::IN_TABLE_BODY;
/* A start tag whose tag name is one of: "td", "th", "tr" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
in_array($token['name'], array('td', 'th', 'tr'))) {
/* Act as if a start tag token with the tag name "tbody" had been
seen, then reprocess the current token. */
$this->emitToken(array(
'name' => 'tbody',
'type' => HTML5_Tokenizer::STARTTAG,
'attr' => array()
));
$this->emitToken($token);
/* A start tag whose tag name is "table" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
$token['name'] === 'table') {
/* Parse error. Act as if an end tag token with the tag name "table"
had been seen, then, if that token wasn't ignored, reprocess the
current token. */
$this->emitToken(array(
'name' => 'table',
'type' => HTML5_Tokenizer::ENDTAG
));
if (!$this->ignored) $this->emitToken($token);
/* An end tag whose tag name is "table" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
$token['name'] === 'table') {
/* If the stack of open elements does not have an element in table
scope with the same tag name as the token, this is a parse error.
Ignore the token. (fragment case) */
if(!$this->elementInScope($token['name'], self::SCOPE_TABLE)) {
$this->ignored = true;
/* Otherwise: */
} else {
do {
$node = array_pop($this->stack);
} while ($node->tagName !== 'table');
/* Reset the insertion mode appropriately. */
$this->resetInsertionMode();
}
/* An end tag whose tag name is one of: "body", "caption", "col",
"colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
array('body', 'caption', 'col', 'colgroup', 'html', 'tbody', 'td',
'tfoot', 'th', 'thead', 'tr'))) {
// Parse error. Ignore the token.
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
($token['name'] === 'style' || $token['name'] === 'script')) {
$this->processWithRulesFor($token, self::IN_HEAD);
} elseif ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'input' &&
// assignment is intentional
/* If the token does not have an attribute with the name "type", or
* if it does, but that attribute's value is not an ASCII
* case-insensitive match for the string "hidden", then: act as
* described in the "anything else" entry below. */
($type = $this->getAttr($token, 'type')) && strtolower($type) === 'hidden') {
// I.e., if its an input with the type attribute == 'hidden'
/* Otherwise */
// parse error
$this->insertElement($token);
array_pop($this->stack);
} elseif ($token['type'] === HTML5_Tokenizer::EOF) {
/* If the current node is not the root html element, then this is a parse error. */
if (end($this->stack)->tagName !== 'html') {
// Note: It can only be the current node in the fragment case.
// parse error
}
/* Stop parsing. */
/* Anything else */
} else {
/* Parse error. Process the token as if the insertion mode was "in
body", with the following exception: */
$old = $this->foster_parent;
$this->foster_parent = true;
$this->processWithRulesFor($token, self::IN_BODY);
$this->foster_parent = $old;
}
break;
case self::IN_TABLE_TEXT:
/* A character token */
if($token['type'] === HTML5_Tokenizer::CHARACTER) {
/* Append the character token to the pending table
* character tokens list. */
$this->pendingTableCharacters .= $token['data'];
$this->pendingTableCharactersDirty = true;
} elseif ($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
$this->pendingTableCharacters .= $token['data'];
/* Anything else */
} else {
if ($this->pendingTableCharacters !== '' && is_string($this->pendingTableCharacters)) {
/* If any of the tokens in the pending table character tokens list
* are character tokens that are not one of U+0009 CHARACTER
* TABULATION, U+000A LINE FEED (LF), U+000C FORM FEED (FF), or
* U+0020 SPACE, then reprocess those character tokens using the
* rules given in the "anything else" entry in the in table"
* insertion mode.*/
if ($this->pendingTableCharactersDirty) {
/* Parse error. Process the token using the rules for the
* "in body" insertion mode, except that if the current
* node is a table, tbody, tfoot, thead, or tr element,
* then, whenever a node would be inserted into the current
* node, it must instead be foster parented. */
// XERROR
$old = $this->foster_parent;
$this->foster_parent = true;
$text_token = array(
'type' => HTML5_Tokenizer::CHARACTER,
'data' => $this->pendingTableCharacters,
);
$this->processWithRulesFor($text_token, self::IN_BODY);
$this->foster_parent = $old;
/* Otherwise, insert the characters given by the pending table
* character tokens list into the current node. */
} else {
$this->insertText($this->pendingTableCharacters);
}
$this->pendingTableCharacters = null;
$this->pendingTableCharactersNull = null;
}
/* Switch the insertion mode to the original insertion mode and
* reprocess the token.
*/
$this->mode = $this->original_mode;
$this->emitToken($token);
}
break;
case self::IN_CAPTION:
/* An end tag whose tag name is "caption" */
if($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'caption') {
/* If the stack of open elements does not have an element in table
scope with the same tag name as the token, this is a parse error.
Ignore the token. (fragment case) */
if(!$this->elementInScope($token['name'], self::SCOPE_TABLE)) {
$this->ignored = true;
// Ignore
/* Otherwise: */
} else {
/* Generate implied end tags. */
$this->generateImpliedEndTags();
/* Now, if the current node is not a caption element, then this
is a parse error. */
// XERROR: implement
/* Pop elements from this stack until a caption element has
been popped from the stack. */
do {
$node = array_pop($this->stack);
} while ($node->tagName !== 'caption');
/* Clear the list of active formatting elements up to the last
marker. */
$this->clearTheActiveFormattingElementsUpToTheLastMarker();
/* Switch the insertion mode to "in table". */
$this->mode = self::IN_TABLE;
}
/* A start tag whose tag name is one of: "caption", "col", "colgroup",
"tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag
name is "table" */
} elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
'thead', 'tr'))) || ($token['type'] === HTML5_Tokenizer::ENDTAG &&
$token['name'] === 'table')) {
/* Parse error. Act as if an end tag with the tag name "caption"
had been seen, then, if that token wasn't ignored, reprocess the
current token. */
$this->emitToken(array(
'name' => 'caption',
'type' => HTML5_Tokenizer::ENDTAG
));
if (!$this->ignored) $this->emitToken($token);
/* An end tag whose tag name is one of: "body", "col", "colgroup",
"html", "tbody", "td", "tfoot", "th", "thead", "tr" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
array('body', 'col', 'colgroup', 'html', 'tbody', 'tfoot', 'th',
'thead', 'tr'))) {
// Parse error. Ignore the token.
$this->ignored = true;
/* Anything else */
} else {
/* Process the token as if the insertion mode was "in body". */
$this->processWithRulesFor($token, self::IN_BODY);
}
break;
case self::IN_COLUMN_GROUP:
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
or U+0020 SPACE */
if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
/* Append the character to the current node. */
$this->insertText($token['data']);
/* A comment token */
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
/* Append a Comment node to the current node with the data
attribute set to the data given in the comment token. */
$this->insertToken($token['data']);
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
// parse error
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
$this->processWithRulesFor($token, self::IN_BODY);
/* A start tag whose tag name is "col" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'col') {
/* Insert a col element for the token. Immediately pop the current
node off the stack of open elements. */
$this->insertElement($token);
array_pop($this->stack);
// XERROR: Acknowledge the token's self-closing flag, if it is set.
/* An end tag whose tag name is "colgroup" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
$token['name'] === 'colgroup') {
/* If the current node is the root html element, then this is a
parse error, ignore the token. (fragment case) */
if(end($this->stack)->tagName === 'html') {
$this->ignored = true;
/* Otherwise, pop the current node (which will be a colgroup
element) from the stack of open elements. Switch the insertion
mode to "in table". */
} else {
array_pop($this->stack);
$this->mode = self::IN_TABLE;
}
/* An end tag whose tag name is "col" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'col') {
/* Parse error. Ignore the token. */
$this->ignored = true;
/* An end-of-file token */
/* If the current node is the root html element */
} elseif($token['type'] === HTML5_Tokenizer::EOF && end($this->stack)->tagName === 'html') {
/* Stop parsing */
/* Anything else */
} else {
/* Act as if an end tag with the tag name "colgroup" had been seen,
and then, if that token wasn't ignored, reprocess the current token. */
$this->emitToken(array(
'name' => 'colgroup',
'type' => HTML5_Tokenizer::ENDTAG
));
if (!$this->ignored) $this->emitToken($token);
}
break;
case self::IN_TABLE_BODY:
$clear = array('tbody', 'tfoot', 'thead', 'html');
/* A start tag whose tag name is "tr" */
if($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'tr') {
/* Clear the stack back to a table body context. */
$this->clearStackToTableContext($clear);
/* Insert a tr element for the token, then switch the insertion
mode to "in row". */
$this->insertElement($token);
$this->mode = self::IN_ROW;
/* A start tag whose tag name is one of: "th", "td" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
($token['name'] === 'th' || $token['name'] === 'td')) {
/* Parse error. Act as if a start tag with the tag name "tr" had
been seen, then reprocess the current token. */
$this->emitToken(array(
'name' => 'tr',
'type' => HTML5_Tokenizer::STARTTAG,
'attr' => array()
));
$this->emitToken($token);
/* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
in_array($token['name'], array('tbody', 'tfoot', 'thead'))) {
/* If the stack of open elements does not have an element in table
scope with the same tag name as the token, this is a parse error.
Ignore the token. */
if(!$this->elementInScope($token['name'], self::SCOPE_TABLE)) {
// Parse error
$this->ignored = true;
/* Otherwise: */
} else {
/* Clear the stack back to a table body context. */
$this->clearStackToTableContext($clear);
/* Pop the current node from the stack of open elements. Switch
the insertion mode to "in table". */
array_pop($this->stack);
$this->mode = self::IN_TABLE;
}
/* A start tag whose tag name is one of: "caption", "col", "colgroup",
"tbody", "tfoot", "thead", or an end tag whose tag name is "table" */
} elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead'))) ||
($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'table')) {
/* If the stack of open elements does not have a tbody, thead, or
tfoot element in table scope, this is a parse error. Ignore the
token. (fragment case) */
if(!$this->elementInScope(array('tbody', 'thead', 'tfoot'), self::SCOPE_TABLE)) {
// parse error
$this->ignored = true;
/* Otherwise: */
} else {
/* Clear the stack back to a table body context. */
$this->clearStackToTableContext($clear);
/* Act as if an end tag with the same tag name as the current
node ("tbody", "tfoot", or "thead") had been seen, then
reprocess the current token. */
$this->emitToken(array(
'name' => end($this->stack)->tagName,
'type' => HTML5_Tokenizer::ENDTAG
));
$this->emitToken($token);
}
/* An end tag whose tag name is one of: "body", "caption", "col",
"colgroup", "html", "td", "th", "tr" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) {
/* Parse error. Ignore the token. */
$this->ignored = true;
/* Anything else */
} else {
/* Process the token as if the insertion mode was "in table". */
$this->processWithRulesFor($token, self::IN_TABLE);
}
break;
case self::IN_ROW:
$clear = array('tr', 'html');
/* A start tag whose tag name is one of: "th", "td" */
if($token['type'] === HTML5_Tokenizer::STARTTAG &&
($token['name'] === 'th' || $token['name'] === 'td')) {
/* Clear the stack back to a table row context. */
$this->clearStackToTableContext($clear);
/* Insert an HTML element for the token, then switch the insertion
mode to "in cell". */
$this->insertElement($token);
$this->mode = self::IN_CELL;
/* Insert a marker at the end of the list of active formatting
elements. */
$this->a_formatting[] = self::MARKER;
/* An end tag whose tag name is "tr" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'tr') {
/* If the stack of open elements does not have an element in table
scope with the same tag name as the token, this is a parse error.
Ignore the token. (fragment case) */
if(!$this->elementInScope($token['name'], self::SCOPE_TABLE)) {
// Ignore.
$this->ignored = true;
/* Otherwise: */
} else {
/* Clear the stack back to a table row context. */
$this->clearStackToTableContext($clear);
/* Pop the current node (which will be a tr element) from the
stack of open elements. Switch the insertion mode to "in table
body". */
array_pop($this->stack);
$this->mode = self::IN_TABLE_BODY;
}
/* A start tag whose tag name is one of: "caption", "col", "colgroup",
"tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */
} elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr'))) ||
($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'table')) {
/* Act as if an end tag with the tag name "tr" had been seen, then,
if that token wasn't ignored, reprocess the current token. */
$this->emitToken(array(
'name' => 'tr',
'type' => HTML5_Tokenizer::ENDTAG
));
if (!$this->ignored) $this->emitToken($token);
/* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
in_array($token['name'], array('tbody', 'tfoot', 'thead'))) {
/* If the stack of open elements does not have an element in table
scope with the same tag name as the token, this is a parse error.
Ignore the token. */
if(!$this->elementInScope($token['name'], self::SCOPE_TABLE)) {
$this->ignored = true;
/* Otherwise: */
} else {
/* Otherwise, act as if an end tag with the tag name "tr" had
been seen, then reprocess the current token. */
$this->emitToken(array(
'name' => 'tr',
'type' => HTML5_Tokenizer::ENDTAG
));
$this->emitToken($token);
}
/* An end tag whose tag name is one of: "body", "caption", "col",
"colgroup", "html", "td", "th" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th'))) {
/* Parse error. Ignore the token. */
$this->ignored = true;
/* Anything else */
} else {
/* Process the token as if the insertion mode was "in table". */
$this->processWithRulesFor($token, self::IN_TABLE);
}
break;
case self::IN_CELL:
/* An end tag whose tag name is one of: "td", "th" */
if($token['type'] === HTML5_Tokenizer::ENDTAG &&
($token['name'] === 'td' || $token['name'] === 'th')) {
/* If the stack of open elements does not have an element in table
scope with the same tag name as that of the token, then this is a
parse error and the token must be ignored. */
if(!$this->elementInScope($token['name'], self::SCOPE_TABLE)) {
$this->ignored = true;
/* Otherwise: */
} else {
/* Generate implied end tags, except for elements with the same
tag name as the token. */
$this->generateImpliedEndTags(array($token['name']));
/* Now, if the current node is not an element with the same tag
name as the token, then this is a parse error. */
// XERROR: Implement parse error code
/* Pop elements from this stack until an element with the same
tag name as the token has been popped from the stack. */
do {
$node = array_pop($this->stack);
} while ($node->tagName !== $token['name']);
/* Clear the list of active formatting elements up to the last
marker. */
$this->clearTheActiveFormattingElementsUpToTheLastMarker();
/* Switch the insertion mode to "in row". (The current node
will be a tr element at this point.) */
$this->mode = self::IN_ROW;
}
/* A start tag whose tag name is one of: "caption", "col", "colgroup",
"tbody", "td", "tfoot", "th", "thead", "tr" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
'thead', 'tr'))) {
/* If the stack of open elements does not have a td or th element
in table scope, then this is a parse error; ignore the token.
(fragment case) */
if(!$this->elementInScope(array('td', 'th'), self::SCOPE_TABLE)) {
// parse error
$this->ignored = true;
/* Otherwise, close the cell (see below) and reprocess the current
token. */
} else {
$this->closeCell();
$this->emitToken($token);
}
/* An end tag whose tag name is one of: "body", "caption", "col",
"colgroup", "html" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
array('body', 'caption', 'col', 'colgroup', 'html'))) {
/* Parse error. Ignore the token. */
$this->ignored = true;
/* An end tag whose tag name is one of: "table", "tbody", "tfoot",
"thead", "tr" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
/* If the stack of open elements does not have a td or th element
in table scope, then this is a parse error; ignore the token.
(innerHTML case) */
if(!$this->elementInScope(array('td', 'th'), self::SCOPE_TABLE)) {
// Parse error
$this->ignored = true;
/* Otherwise, close the cell (see below) and reprocess the current
token. */
} else {
$this->closeCell();
$this->emitToken($token);
}
/* Anything else */
} else {
/* Process the token as if the insertion mode was "in body". */
$this->processWithRulesFor($token, self::IN_BODY);
}
break;
case self::IN_SELECT:
/* Handle the token as follows: */
/* A character token */
if(
$token['type'] === HTML5_Tokenizer::CHARACTER ||
$token['type'] === HTML5_Tokenizer::SPACECHARACTER
) {
/* Append the token's character to the current node. */
$this->insertText($token['data']);
/* A comment token */
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
/* Append a Comment node to the current node with the data
attribute set to the data given in the comment token. */
$this->insertComment($token['data']);
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
// parse error
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
$this->processWithRulesFor($token, self::INBODY);
/* A start tag token whose tag name is "option" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
$token['name'] === 'option') {
/* If the current node is an option element, act as if an end tag
with the tag name "option" had been seen. */
if(end($this->stack)->tagName === 'option') {
$this->emitToken(array(
'name' => 'option',
'type' => HTML5_Tokenizer::ENDTAG
));
}
/* Insert an HTML element for the token. */
$this->insertElement($token);
/* A start tag token whose tag name is "optgroup" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
$token['name'] === 'optgroup') {
/* If the current node is an option element, act as if an end tag
with the tag name "option" had been seen. */
if(end($this->stack)->tagName === 'option') {
$this->emitToken(array(
'name' => 'option',
'type' => HTML5_Tokenizer::ENDTAG
));
}
/* If the current node is an optgroup element, act as if an end tag
with the tag name "optgroup" had been seen. */
if(end($this->stack)->tagName === 'optgroup') {
$this->emitToken(array(
'name' => 'optgroup',
'type' => HTML5_Tokenizer::ENDTAG
));
}
/* Insert an HTML element for the token. */
$this->insertElement($token);
/* An end tag token whose tag name is "optgroup" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
$token['name'] === 'optgroup') {
/* First, if the current node is an option element, and the node
immediately before it in the stack of open elements is an optgroup
element, then act as if an end tag with the tag name "option" had
been seen. */
$elements_in_stack = count($this->stack);
if($this->stack[$elements_in_stack - 1]->tagName === 'option' &&
$this->stack[$elements_in_stack - 2]->tagName === 'optgroup') {
$this->emitToken(array(
'name' => 'option',
'type' => HTML5_Tokenizer::ENDTAG
));
}
/* If the current node is an optgroup element, then pop that node
from the stack of open elements. Otherwise, this is a parse error,
ignore the token. */
if(end($this->stack)->tagName === 'optgroup') {
array_pop($this->stack);
} else {
// parse error
$this->ignored = true;
}
/* An end tag token whose tag name is "option" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
$token['name'] === 'option') {
/* If the current node is an option element, then pop that node
from the stack of open elements. Otherwise, this is a parse error,
ignore the token. */
if(end($this->stack)->tagName === 'option') {
array_pop($this->stack);
} else {
// parse error
$this->ignored = true;
}
/* An end tag whose tag name is "select" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
$token['name'] === 'select') {
/* If the stack of open elements does not have an element in table
scope with the same tag name as the token, this is a parse error.
Ignore the token. (fragment case) */
if(!$this->elementInScope($token['name'], self::SCOPE_TABLE)) {
$this->ignored = true;
// parse error
/* Otherwise: */
} else {
/* Pop elements from the stack of open elements until a select
element has been popped from the stack. */
do {
$node = array_pop($this->stack);
} while ($node->tagName !== 'select');
/* Reset the insertion mode appropriately. */
$this->resetInsertionMode();
}
/* A start tag whose tag name is "select" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'select') {
/* Parse error. Act as if the token had been an end tag with the
tag name "select" instead. */
$this->emitToken(array(
'name' => 'select',
'type' => HTML5_Tokenizer::ENDTAG
));
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
($token['name'] === 'input' || $token['name'] === 'keygen' || $token['name'] === 'textarea')) {
// parse error
$this->emitToken(array(
'name' => 'select',
'type' => HTML5_Tokenizer::ENDTAG
));
$this->emitToken($token);
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'script') {
$this->processWithRulesFor($token, self::IN_HEAD);
} elseif($token['type'] === HTML5_Tokenizer::EOF) {
// XERROR: If the current node is not the root html element, then this is a parse error.
/* Stop parsing */
/* Anything else */
} else {
/* Parse error. Ignore the token. */
$this->ignored = true;
}
break;
case self::IN_SELECT_IN_TABLE:
if($token['type'] === HTML5_Tokenizer::STARTTAG &&
in_array($token['name'], array('caption', 'table', 'tbody',
'tfoot', 'thead', 'tr', 'td', 'th'))) {
// parse error
$this->emitToken(array(
'name' => 'select',
'type' => HTML5_Tokenizer::ENDTAG,
));
$this->emitToken($token);
/* An end tag whose tag name is one of: "caption", "table", "tbody",
"tfoot", "thead", "tr", "td", "th" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
in_array($token['name'], array('caption', 'table', 'tbody', 'tfoot', 'thead', 'tr', 'td', 'th'))) {
/* Parse error. */
// parse error
/* If the stack of open elements has an element in table scope with
the same tag name as that of the token, then act as if an end tag
with the tag name "select" had been seen, and reprocess the token.
Otherwise, ignore the token. */
if($this->elementInScope($token['name'], self::SCOPE_TABLE)) {
$this->emitToken(array(
'name' => 'select',
'type' => HTML5_Tokenizer::ENDTAG
));
$this->emitToken($token);
} else {
$this->ignored = true;
}
} else {
$this->processWithRulesFor($token, self::IN_SELECT);
}
break;
case self::IN_FOREIGN_CONTENT:
if ($token['type'] === HTML5_Tokenizer::CHARACTER ||
$token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
$this->insertText($token['data']);
} elseif ($token['type'] === HTML5_Tokenizer::COMMENT) {
$this->insertComment($token['data']);
} elseif ($token['type'] === HTML5_Tokenizer::DOCTYPE) {
// XERROR: parse error
} elseif ($token['type'] === HTML5_Tokenizer::ENDTAG &&
$token['name'] === 'script' && end($this->stack)->tagName === 'script' &&
// XDOM
end($this->stack)->namespaceURI === self::NS_SVG) {
array_pop($this->stack);
// a bunch of script running mumbo jumbo
} elseif (
($token['type'] === HTML5_Tokenizer::STARTTAG &&
((
$token['name'] !== 'mglyph' &&
$token['name'] !== 'malignmark' &&
// XDOM
end($this->stack)->namespaceURI === self::NS_MATHML &&
in_array(end($this->stack)->tagName, array('mi', 'mo', 'mn', 'ms', 'mtext'))
) ||
(
$token['name'] === 'svg' &&
// XDOM
end($this->stack)->namespaceURI === self::NS_MATHML &&
end($this->stack)->tagName === 'annotation-xml'
) ||
(
// XDOM
end($this->stack)->namespaceURI === self::NS_SVG &&
in_array(end($this->stack)->tagName, array('foreignObject', 'desc', 'title'))
) ||
(
// XSKETCHY && XDOM
end($this->stack)->namespaceURI === self::NS_HTML
))
) || $token['type'] === HTML5_Tokenizer::ENDTAG
) {
$this->processWithRulesFor($token, $this->secondary_mode);
/* If, after doing so, the insertion mode is still "in foreign
* content", but there is no element in scope that has a namespace
* other than the HTML namespace, switch the insertion mode to the
* secondary insertion mode. */
if ($this->mode === self::IN_FOREIGN_CONTENT) {
$found = false;
// this basically duplicates elementInScope()
for ($i = count($this->stack) - 1; $i >= 0; $i--) {
// XDOM
$node = $this->stack[$i];
if ($node->namespaceURI !== self::NS_HTML) {
$found = true;
break;
} elseif (in_array($node->tagName, array('table', 'html',
'applet', 'caption', 'td', 'th', 'button', 'marquee',
'object')) || ($node->tagName === 'foreignObject' &&
$node->namespaceURI === self::NS_SVG)) {
break;
}
}
if (!$found) {
$this->mode = $this->secondary_mode;
}
}
} elseif ($token['type'] === HTML5_Tokenizer::EOF || (
$token['type'] === HTML5_Tokenizer::STARTTAG &&
(in_array($token['name'], array('b', "big", "blockquote", "body", "br",
"center", "code", "dc", "dd", "div", "dl", "ds", "dt", "em", "embed", "h1", "h2",
"h3", "h4", "h5", "h6", "head", "hr", "i", "img", "li", "listing",
"menu", "meta", "nobr", "ol", "p", "pre", "ruby", "s", "small",
"span", "strong", "strike", "sub", "sup", "table", "tt", "u", "ul",
"var")) || ($token['name'] === 'font' && ($this->getAttr($token, 'color') ||
$this->getAttr($token, 'face') || $this->getAttr($token, 'size')))))) {
// XERROR: parse error
do {
$node = array_pop($this->stack);
// XDOM
} while ($node->namespaceURI !== self::NS_HTML);
$this->stack[] = $node;
$this->mode = $this->secondary_mode;
$this->emitToken($token);
} elseif ($token['type'] === HTML5_Tokenizer::STARTTAG) {
static $svg_lookup = array(
'altglyph' => 'altGlyph',
'altglyphdef' => 'altGlyphDef',
'altglyphitem' => 'altGlyphItem',
'animatecolor' => 'animateColor',
'animatemotion' => 'animateMotion',
'animatetransform' => 'animateTransform',
'clippath' => 'clipPath',
'feblend' => 'feBlend',
'fecolormatrix' => 'feColorMatrix',
'fecomponenttransfer' => 'feComponentTransfer',
'fecomposite' => 'feComposite',
'feconvolvematrix' => 'feConvolveMatrix',
'fediffuselighting' => 'feDiffuseLighting',
'fedisplacementmap' => 'feDisplacementMap',
'fedistantlight' => 'feDistantLight',
'feflood' => 'feFlood',
'fefunca' => 'feFuncA',
'fefuncb' => 'feFuncB',
'fefuncg' => 'feFuncG',
'fefuncr' => 'feFuncR',
'fegaussianblur' => 'feGaussianBlur',
'feimage' => 'feImage',
'femerge' => 'feMerge',
'femergenode' => 'feMergeNode',
'femorphology' => 'feMorphology',
'feoffset' => 'feOffset',
'fepointlight' => 'fePointLight',
'fespecularlighting' => 'feSpecularLighting',
'fespotlight' => 'feSpotLight',
'fetile' => 'feTile',
'feturbulence' => 'feTurbulence',
'foreignobject' => 'foreignObject',
'glyphref' => 'glyphRef',
'lineargradient' => 'linearGradient',
'radialgradient' => 'radialGradient',
'textpath' => 'textPath',
);
// XDOM
$current = end($this->stack);
if ($current->namespaceURI === self::NS_MATHML) {
$token = $this->adjustMathMLAttributes($token);
}
if ($current->namespaceURI === self::NS_SVG &&
isset($svg_lookup[$token['name']])) {
$token['name'] = $svg_lookup[$token['name']];
}
if ($current->namespaceURI === self::NS_SVG) {
$token = $this->adjustSVGAttributes($token);
}
$token = $this->adjustForeignAttributes($token);
$this->insertForeignElement($token, $current->namespaceURI);
if (isset($token['self-closing'])) {
array_pop($this->stack);
// XERROR: acknowledge self-closing flag
}
}
break;
case self::AFTER_BODY:
/* Handle the token as follows: */
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
or U+0020 SPACE */
if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
/* Process the token as it would be processed if the insertion mode
was "in body". */
$this->processWithRulesFor($token, self::IN_BODY);
/* A comment token */
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
/* Append a Comment node to the first element in the stack of open
elements (the html element), with the data attribute set to the
data given in the comment token. */
// XDOM
$comment = $this->dom->createComment($token['data']);
$this->stack[0]->appendChild($comment);
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
// parse error
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
$this->processWithRulesFor($token, self::IN_BODY);
/* An end tag with the tag name "html" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'html') {
/* If the parser was originally created as part of the HTML
* fragment parsing algorithm, this is a parse error; ignore
* the token. (fragment case) */
$this->ignored = true;
// XERROR: implement this
$this->mode = self::AFTER_AFTER_BODY;
} elseif($token['type'] === HTML5_Tokenizer::EOF) {
/* Stop parsing */
/* Anything else */
} else {
/* Parse error. Set the insertion mode to "in body" and reprocess
the token. */
$this->mode = self::IN_BODY;
$this->emitToken($token);
}
break;
case self::IN_FRAMESET:
/* Handle the token as follows: */
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
/* Append the character to the current node. */
$this->insertText($token['data']);
/* A comment token */
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
/* Append a Comment node to the current node with the data
attribute set to the data given in the comment token. */
$this->insertComment($token['data']);
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
// parse error
/* A start tag with the tag name "frameset" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
$token['name'] === 'frameset') {
$this->insertElement($token);
/* An end tag with the tag name "frameset" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
$token['name'] === 'frameset') {
/* If the current node is the root html element, then this is a
parse error; ignore the token. (fragment case) */
if(end($this->stack)->tagName === 'html') {
$this->ignored = true;
// Parse error
} else {
/* Otherwise, pop the current node from the stack of open
elements. */
array_pop($this->stack);
/* If the parser was not originally created as part of the HTML
* fragment parsing algorithm (fragment case), and the current
* node is no longer a frameset element, then switch the
* insertion mode to "after frameset". */
$this->mode = self::AFTER_FRAMESET;
}
/* A start tag with the tag name "frame" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
$token['name'] === 'frame') {
/* Insert an HTML element for the token. */
$this->insertElement($token);
/* Immediately pop the current node off the stack of open elements. */
array_pop($this->stack);
// XERROR: Acknowledge the token's self-closing flag, if it is set.
/* A start tag with the tag name "noframes" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
$token['name'] === 'noframes') {
/* Process the token using the rules for the "in head" insertion mode. */
$this->processwithRulesFor($token, self::IN_HEAD);
} elseif($token['type'] === HTML5_Tokenizer::EOF) {
// XERROR: If the current node is not the root html element, then this is a parse error.
/* Stop parsing */
/* Anything else */
} else {
/* Parse error. Ignore the token. */
$this->ignored = true;
}
break;
case self::AFTER_FRAMESET:
/* Handle the token as follows: */
/* A character token that is one of one of U+0009 CHARACTER TABULATION,
U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
/* Append the character to the current node. */
$this->insertText($token['data']);
/* A comment token */
} elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
/* Append a Comment node to the current node with the data
attribute set to the data given in the comment token. */
$this->insertComment($token['data']);
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
// parse error
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
$this->processWithRulesFor($token, self::IN_BODY);
/* An end tag with the tag name "html" */
} elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
$token['name'] === 'html') {
$this->mode = self::AFTER_AFTER_FRAMESET;
/* A start tag with the tag name "noframes" */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
$token['name'] === 'noframes') {
$this->processWithRulesFor($token, self::IN_HEAD);
} elseif($token['type'] === HTML5_Tokenizer::EOF) {
/* Stop parsing */
/* Anything else */
} else {
/* Parse error. Ignore the token. */
$this->ignored = true;
}
break;
case self::AFTER_AFTER_BODY:
/* A comment token */
if($token['type'] === HTML5_Tokenizer::COMMENT) {
/* Append a Comment node to the Document object with the data
attribute set to the data given in the comment token. */
// XDOM
$comment = $this->dom->createComment($token['data']);
$this->dom->appendChild($comment);
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE ||
$token['type'] === HTML5_Tokenizer::SPACECHARACTER ||
($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html')) {
$this->processWithRulesFor($token, self::IN_BODY);
/* An end-of-file token */
} elseif($token['type'] === HTML5_Tokenizer::EOF) {
/* OMG DONE!! */
} else {
// parse error
$this->mode = self::IN_BODY;
$this->emitToken($token);
}
break;
case self::AFTER_AFTER_FRAMESET:
/* A comment token */
if($token['type'] === HTML5_Tokenizer::COMMENT) {
/* Append a Comment node to the Document object with the data
attribute set to the data given in the comment token. */
// XDOM
$comment = $this->dom->createComment($token['data']);
$this->dom->appendChild($comment);
} elseif($token['type'] === HTML5_Tokenizer::DOCTYPE ||
$token['type'] === HTML5_Tokenizer::SPACECHARACTER ||
($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html')) {
$this->processWithRulesFor($token, self::IN_BODY);
/* An end-of-file token */
} elseif($token['type'] === HTML5_Tokenizer::EOF) {
/* OMG DONE!! */
} elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'nofrmaes') {
$this->processWithRulesFor($token, self::IN_HEAD);
} else {
// parse error
}
break;
}
// end funky indenting
}
private function insertElement($token, $append = true) {
$el = $this->dom->createElementNS(self::NS_HTML, $token['name']);
if (!empty($token['attr'])) {
foreach($token['attr'] as $attr) {
if(!$el->hasAttribute($attr['name'])) {
$el->setAttribute($attr['name'], $attr['value']);
}
}
}
if ($append) {
$this->appendToRealParent($el);
$this->stack[] = $el;
}
return $el;
}
private function insertText($data) {
if ($data === '') return;
if ($this->ignore_lf_token) {
if ($data[0] === "\n") {
$data = substr($data, 1);
if ($data === false) return;
}
}
$text = $this->dom->createTextNode($data);
$this->appendToRealParent($text);
}
private function insertComment($data) {
$comment = $this->dom->createComment($data);
$this->appendToRealParent($comment);
}
private function appendToRealParent($node) {
// this is only for the foster_parent case
/* If the current node is a table, tbody, tfoot, thead, or tr
element, then, whenever a node would be inserted into the current
node, it must instead be inserted into the foster parent element. */
if(!$this->foster_parent || !in_array(end($this->stack)->tagName,
array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
end($this->stack)->appendChild($node);
} else {
$this->fosterParent($node);
}
}
private function elementInScope($el, $scope = self::SCOPE) {
if(is_array($el)) {
foreach($el as $element) {
if($this->elementInScope($element, $scope)) {
return true;
}
}
return false;
}
$leng = count($this->stack);
for($n = 0; $n < $leng; $n++) {
/* 1. Initialise node to be the current node (the bottommost node of
the stack). */
$node = $this->stack[$leng - 1 - $n];
if($node->tagName === $el) {
/* 2. If node is the target node, terminate in a match state. */
return true;
// We've expanded the logic for these states a little differently;
// Hixie's refactoring into "specific scope" is more general, but
// this "gets the job done"
// these are the common states for all scopes
} elseif($node->tagName === 'table' || $node->tagName === 'html') {
return false;
// these are valid for "in scope" and "in list item scope"
} elseif($scope !== self::SCOPE_TABLE &&
(in_array($node->tagName, array('applet', 'caption', 'td',
'th', 'button', 'marquee', 'object')) ||
$node->tagName === 'foreignObject' && $node->namespaceURI === self::NS_SVG)) {
return false;
// these are valid for "in list item scope"
} elseif($scope === self::SCOPE_LISTITEM && in_array($node->tagName, array('ol', 'ul'))) {
return false;
}
/* Otherwise, set node to the previous entry in the stack of open
elements and return to step 2. (This will never fail, since the loop
will always terminate in the previous step if the top of the stack
is reached.) */
}
}
private function reconstructActiveFormattingElements() {
/* 1. If there are no entries in the list of active formatting elements,
then there is nothing to reconstruct; stop this algorithm. */
$formatting_elements = count($this->a_formatting);
if($formatting_elements === 0) {
return false;
}
/* 3. Let entry be the last (most recently added) element in the list
of active formatting elements. */
$entry = end($this->a_formatting);
/* 2. If the last (most recently added) entry in the list of active
formatting elements is a marker, or if it is an element that is in the
stack of open elements, then there is nothing to reconstruct; stop this
algorithm. */
if($entry === self::MARKER || in_array($entry, $this->stack, true)) {
return false;
}
for($a = $formatting_elements - 1; $a >= 0; true) {
/* 4. If there are no entries before entry in the list of active
formatting elements, then jump to step 8. */
if($a === 0) {
$step_seven = false;
break;
}
/* 5. Let entry be the entry one earlier than entry in the list of
active formatting elements. */
$a--;
$entry = $this->a_formatting[$a];
/* 6. If entry is neither a marker nor an element that is also in
thetack of open elements, go to step 4. */
if($entry === self::MARKER || in_array($entry, $this->stack, true)) {
break;
}
}
while(true) {
/* 7. Let entry be the element one later than entry in the list of
active formatting elements. */
if(isset($step_seven) && $step_seven === true) {
$a++;
$entry = $this->a_formatting[$a];
}
/* 8. Perform a shallow clone of the element entry to obtain clone. */
$clone = $entry->cloneNode();
/* 9. Append clone to the current node and push it onto the stack
of open elements so that it is the new current node. */
$this->appendToRealParent($clone);
$this->stack[] = $clone;
/* 10. Replace the entry for entry in the list with an entry for
clone. */
$this->a_formatting[$a] = $clone;
/* 11. If the entry for clone in the list of active formatting
elements is not the last entry in the list, return to step 7. */
if(end($this->a_formatting) !== $clone) {
$step_seven = true;
} else {
break;
}
}
}
private function clearTheActiveFormattingElementsUpToTheLastMarker() {
/* When the steps below require the UA to clear the list of active
formatting elements up to the last marker, the UA must perform the
following steps: */
while(true) {
/* 1. Let entry be the last (most recently added) entry in the list
of active formatting elements. */
$entry = end($this->a_formatting);
/* 2. Remove entry from the list of active formatting elements. */
array_pop($this->a_formatting);
/* 3. If entry was a marker, then stop the algorithm at this point.
The list has been cleared up to the last marker. */
if($entry === self::MARKER) {
break;
}
}
}
private function generateImpliedEndTags($exclude = array()) {
/* When the steps below require the UA to generate implied end tags,
* then, while the current node is a dc element, a dd element, a ds
* element, a dt element, an li element, an option element, an optgroup
* element, a p element, an rp element, or an rt element, the UA must
* pop the current node off the stack of open elements. */
$node = end($this->stack);
$elements = array_diff(array('dc', 'dd', 'ds', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude);
while(in_array(end($this->stack)->tagName, $elements)) {
array_pop($this->stack);
}
}
private function getElementCategory($node) {
if (!is_object($node)) debug_print_backtrace();
$name = $node->tagName;
if(in_array($name, $this->special))
return self::SPECIAL;
elseif(in_array($name, $this->scoping))
return self::SCOPING;
elseif(in_array($name, $this->formatting))
return self::FORMATTING;
else
return self::PHRASING;
}
private function clearStackToTableContext($elements) {
/* When the steps above require the UA to clear the stack back to a
table context, it means that the UA must, while the current node is not
a table element or an html element, pop elements from the stack of open
elements. */
while(true) {
$name = end($this->stack)->tagName;
if(in_array($name, $elements)) {
break;
} else {
array_pop($this->stack);
}
}
}
private function resetInsertionMode($context = null) {
/* 1. Let last be false. */
$last = false;
$leng = count($this->stack);
for($n = $leng - 1; $n >= 0; $n--) {
/* 2. Let node be the last node in the stack of open elements. */
$node = $this->stack[$n];
/* 3. If node is the first node in the stack of open elements, then
* set last to true and set node to the context element. (fragment
* case) */
if($this->stack[0]->isSameNode($node)) {
$last = true;
$node = $context;
}
/* 4. If node is a select element, then switch the insertion mode to
"in select" and abort these steps. (fragment case) */
if($node->tagName === 'select') {
$this->mode = self::IN_SELECT;
break;
/* 5. If node is a td or th element, then switch the insertion mode
to "in cell" and abort these steps. */
} elseif($node->tagName === 'td' || $node->nodeName === 'th') {
$this->mode = self::IN_CELL;
break;
/* 6. If node is a tr element, then switch the insertion mode to
"in row" and abort these steps. */
} elseif($node->tagName === 'tr') {
$this->mode = self::IN_ROW;
break;
/* 7. If node is a tbody, thead, or tfoot element, then switch the
insertion mode to "in table body" and abort these steps. */
} elseif(in_array($node->tagName, array('tbody', 'thead', 'tfoot'))) {
$this->mode = self::IN_TABLE_BODY;
break;
/* 8. If node is a caption element, then switch the insertion mode
to "in caption" and abort these steps. */
} elseif($node->tagName === 'caption') {
$this->mode = self::IN_CAPTION;
break;
/* 9. If node is a colgroup element, then switch the insertion mode
to "in column group" and abort these steps. (innerHTML case) */
} elseif($node->tagName === 'colgroup') {
$this->mode = self::IN_COLUMN_GROUP;
break;
/* 10. If node is a table element, then switch the insertion mode
to "in table" and abort these steps. */
} elseif($node->tagName === 'table') {
$this->mode = self::IN_TABLE;
break;
/* 11. If node is an element from the MathML namespace or the SVG
* namespace, then switch the insertion mode to "in foreign
* content", let the secondary insertion mode be "in body", and
* abort these steps. */
} elseif($node->namespaceURI === self::NS_SVG ||
$node->namespaceURI === self::NS_MATHML) {
$this->mode = self::IN_FOREIGN_CONTENT;
$this->secondary_mode = self::IN_BODY;
break;
/* 12. If node is a head element, then switch the insertion mode
to "in body" ("in body"! not "in head"!) and abort these steps.
(fragment case) */
} elseif($node->tagName === 'head') {
$this->mode = self::IN_BODY;
break;
/* 13. If node is a body element, then switch the insertion mode to
"in body" and abort these steps. */
} elseif($node->tagName === 'body') {
$this->mode = self::IN_BODY;
break;
/* 14. If node is a frameset element, then switch the insertion
mode to "in frameset" and abort these steps. (fragment case) */
} elseif($node->tagName === 'frameset') {
$this->mode = self::IN_FRAMESET;
break;
/* 15. If node is an html element, then: if the head element
pointer is null, switch the insertion mode to "before head",
otherwise, switch the insertion mode to "after head". In either
case, abort these steps. (fragment case) */
} elseif($node->tagName === 'html') {
$this->mode = ($this->head_pointer === null)
? self::BEFORE_HEAD
: self::AFTER_HEAD;
break;
/* 16. If last is true, then set the insertion mode to "in body"
and abort these steps. (fragment case) */
} elseif($last) {
$this->mode = self::IN_BODY;
break;
}
}
}
private function closeCell() {
/* If the stack of open elements has a td or th element in table scope,
then act as if an end tag token with that tag name had been seen. */
foreach(array('td', 'th') as $cell) {
if($this->elementInScope($cell, self::SCOPE_TABLE)) {
$this->emitToken(array(
'name' => $cell,
'type' => HTML5_Tokenizer::ENDTAG
));
break;
}
}
}
private function processWithRulesFor($token, $mode) {
/* "using the rules for the m insertion mode", where m is one of these
* modes, the user agent must use the rules described under the m
* insertion mode's section, but must leave the insertion mode
* unchanged unless the rules in m themselves switch the insertion mode
* to a new value. */
return $this->emitToken($token, $mode);
}
private function insertCDATAElement($token) {
$this->insertElement($token);
$this->original_mode = $this->mode;
$this->mode = self::IN_CDATA_RCDATA;
$this->content_model = HTML5_Tokenizer::CDATA;
}
private function insertRCDATAElement($token) {
$this->insertElement($token);
$this->original_mode = $this->mode;
$this->mode = self::IN_CDATA_RCDATA;
$this->content_model = HTML5_Tokenizer::RCDATA;
}
private function getAttr($token, $key) {
if (!isset($token['attr'])) return false;
$ret = false;
foreach ($token['attr'] as $keypair) {
if ($keypair['name'] === $key) $ret = $keypair['value'];
}
return $ret;
}
private function getCurrentTable() {
/* The current table is the last table element in the stack of open
* elements, if there is one. If there is no table element in the stack
* of open elements (fragment case), then the current table is the
* first element in the stack of open elements (the html element). */
for ($i = count($this->stack) - 1; $i >= 0; $i--) {
if ($this->stack[$i]->tagName === 'table') {
return $this->stack[$i];
}
}
return $this->stack[0];
}
private function getFosterParent() {
/* The foster parent element is the parent element of the last
table element in the stack of open elements, if there is a
table element and it has such a parent element. If there is no
table element in the stack of open elements (innerHTML case),
then the foster parent element is the first element in the
stack of open elements (the html element). Otherwise, if there
is a table element in the stack of open elements, but the last
table element in the stack of open elements has no parent, or
its parent node is not an element, then the foster parent
element is the element before the last table element in the
stack of open elements. */
for($n = count($this->stack) - 1; $n >= 0; $n--) {
if($this->stack[$n]->tagName === 'table') {
$table = $this->stack[$n];
break;
}
}
if(isset($table) && $table->parentNode !== null) {
return $table->parentNode;
} elseif(!isset($table)) {
return $this->stack[0];
} elseif(isset($table) && ($table->parentNode === null ||
$table->parentNode->nodeType !== XML_ELEMENT_NODE)) {
return $this->stack[$n - 1];
}
}
public function fosterParent($node) {
$foster_parent = $this->getFosterParent();
$table = $this->getCurrentTable(); // almost equivalent to last table element, except it can be html
/* When a node node is to be foster parented, the node node must be
* be inserted into the foster parent element. */
/* If the foster parent element is the parent element of the last table
* element in the stack of open elements, then node must be inserted
* immediately before the last table element in the stack of open
* elements in the foster parent element; otherwise, node must be
* appended to the foster parent element. */
if ($table->tagName === 'table' && $table->parentNode->isSameNode($foster_parent)) {
$foster_parent->insertBefore($node, $table);
} else {
$foster_parent->appendChild($node);
}
}
/**
* For debugging, prints the stack
*/
private function printStack() {
$names = array();
foreach ($this->stack as $i => $element) {
$names[] = $element->tagName;
}
echo " -> stack [" . implode(', ', $names) . "]\n";
}
/**
* For debugging, prints active formatting elements
*/
private function printActiveFormattingElements() {
if (!$this->a_formatting) return;
$names = array();
foreach ($this->a_formatting as $node) {
if ($node === self::MARKER) $names[] = 'MARKER';
else $names[] = $node->tagName;
}
echo " -> active formatting [" . implode(', ', $names) . "]\n";
}
public function currentTableIsTainted() {
return !empty($this->getCurrentTable()->tainted);
}
/**
* Sets up the tree constructor for building a fragment.
*/
public function setupContext($context = null) {
$this->fragment = true;
if ($context) {
$context = $this->dom->createElementNS(self::NS_HTML, $context);
/* 4.1. Set the HTML parser's tokenization stage's content model
* flag according to the context element, as follows: */
switch ($context->tagName) {
case 'title': case 'textarea':
$this->content_model = HTML5_Tokenizer::RCDATA;
break;
case 'style': case 'script': case 'xmp': case 'iframe':
case 'noembed': case 'noframes':
$this->content_model = HTML5_Tokenizer::CDATA;
break;
case 'noscript':
// XSCRIPT: assuming scripting is enabled
$this->content_model = HTML5_Tokenizer::CDATA;
break;
case 'plaintext':
$this->content_model = HTML5_Tokenizer::PLAINTEXT;
break;
}
/* 4.2. Let root be a new html element with no attributes. */
$root = $this->dom->createElementNS(self::NS_HTML, 'html');
$this->root = $root;
/* 4.3 Append the element root to the Document node created above. */
$this->dom->appendChild($root);
/* 4.4 Set up the parser's stack of open elements so that it
* contains just the single element root. */
$this->stack = array($root);
/* 4.5 Reset the parser's insertion mode appropriately. */
$this->resetInsertionMode($context);
/* 4.6 Set the parser's form element pointer to the nearest node
* to the context element that is a form element (going straight up
* the ancestor chain, and including the element itself, if it is a
* form element), or, if there is no such form element, to null. */
$node = $context;
do {
if ($node->tagName === 'form') {
$this->form_pointer = $node;
break;
}
} while ($node = $node->parentNode);
}
}
public function adjustMathMLAttributes($token) {
foreach ($token['attr'] as &$kp) {
if ($kp['name'] === 'definitionurl') {
$kp['name'] = 'definitionURL';
}
}
return $token;
}
public function adjustSVGAttributes($token) {
static $lookup = array(
'attributename' => 'attributeName',
'attributetype' => 'attributeType',
'basefrequency' => 'baseFrequency',
'baseprofile' => 'baseProfile',
'calcmode' => 'calcMode',
'clippathunits' => 'clipPathUnits',
'contentscripttype' => 'contentScriptType',
'contentstyletype' => 'contentStyleType',
'diffuseconstant' => 'diffuseConstant',
'edgemode' => 'edgeMode',
'externalresourcesrequired' => 'externalResourcesRequired',
'filterres' => 'filterRes',
'filterunits' => 'filterUnits',
'glyphref' => 'glyphRef',
'gradienttransform' => 'gradientTransform',
'gradientunits' => 'gradientUnits',
'kernelmatrix' => 'kernelMatrix',
'kernelunitlength' => 'kernelUnitLength',
'keypoints' => 'keyPoints',
'keysplines' => 'keySplines',
'keytimes' => 'keyTimes',
'lengthadjust' => 'lengthAdjust',
'limitingconeangle' => 'limitingConeAngle',
'markerheight' => 'markerHeight',
'markerunits' => 'markerUnits',
'markerwidth' => 'markerWidth',
'maskcontentunits' => 'maskContentUnits',
'maskunits' => 'maskUnits',
'numoctaves' => 'numOctaves',
'pathlength' => 'pathLength',
'patterncontentunits' => 'patternContentUnits',
'patterntransform' => 'patternTransform',
'patternunits' => 'patternUnits',
'pointsatx' => 'pointsAtX',
'pointsaty' => 'pointsAtY',
'pointsatz' => 'pointsAtZ',
'preservealpha' => 'preserveAlpha',
'preserveaspectratio' => 'preserveAspectRatio',
'primitiveunits' => 'primitiveUnits',
'refx' => 'refX',
'refy' => 'refY',
'repeatcount' => 'repeatCount',
'repeatdur' => 'repeatDur',
'requiredextensions' => 'requiredExtensions',
'requiredfeatures' => 'requiredFeatures',
'specularconstant' => 'specularConstant',
'specularexponent' => 'specularExponent',
'spreadmethod' => 'spreadMethod',
'startoffset' => 'startOffset',
'stddeviation' => 'stdDeviation',
'stitchtiles' => 'stitchTiles',
'surfacescale' => 'surfaceScale',
'systemlanguage' => 'systemLanguage',
'tablevalues' => 'tableValues',
'targetx' => 'targetX',
'targety' => 'targetY',
'textlength' => 'textLength',
'viewbox' => 'viewBox',
'viewtarget' => 'viewTarget',
'xchannelselector' => 'xChannelSelector',
'ychannelselector' => 'yChannelSelector',
'zoomandpan' => 'zoomAndPan',
);
foreach ($token['attr'] as &$kp) {
if (isset($lookup[$kp['name']])) {
$kp['name'] = $lookup[$kp['name']];
}
}
return $token;
}
public function adjustForeignAttributes($token) {
static $lookup = array(
'xlink:actuate' => array('xlink', 'actuate', self::NS_XLINK),
'xlink:arcrole' => array('xlink', 'arcrole', self::NS_XLINK),
'xlink:href' => array('xlink', 'href', self::NS_XLINK),
'xlink:role' => array('xlink', 'role', self::NS_XLINK),
'xlink:show' => array('xlink', 'show', self::NS_XLINK),
'xlink:title' => array('xlink', 'title', self::NS_XLINK),
'xlink:type' => array('xlink', 'type', self::NS_XLINK),
'xml:base' => array('xml', 'base', self::NS_XML),
'xml:lang' => array('xml', 'lang', self::NS_XML),
'xml:space' => array('xml', 'space', self::NS_XML),
'xmlns' => array(null, 'xmlns', self::NS_XMLNS),
'xmlns:xlink' => array('xmlns', 'xlink', self::NS_XMLNS),
);
foreach ($token['attr'] as &$kp) {
if (isset($lookup[$kp['name']])) {
$kp['name'] = $lookup[$kp['name']];
}
}
return $token;
}
public function insertForeignElement($token, $namespaceURI) {
$el = $this->dom->createElementNS($namespaceURI, $token['name']);
if (!empty($token['attr'])) {
foreach ($token['attr'] as $kp) {
$attr = $kp['name'];
if (is_array($attr)) {
$ns = $attr[2];
$attr = $attr[1];
} else {
$ns = self::NS_HTML;
}
if (!$el->hasAttributeNS($ns, $attr)) {
// XSKETCHY: work around godawful libxml bug
if ($ns === self::NS_XLINK) {
$el->setAttribute('xlink:'.$attr, $kp['value']);
} elseif ($ns === self::NS_HTML) {
// Another godawful libxml bug
$el->setAttribute($attr, $kp['value']);
} else {
$el->setAttributeNS($ns, $attr, $kp['value']);
}
}
}
}
$this->appendToRealParent($el);
$this->stack[] = $el;
// XERROR: see below
/* If the newly created element has an xmlns attribute in the XMLNS
* namespace whose value is not exactly the same as the element's
* namespace, that is a parse error. Similarly, if the newly created
* element has an xmlns:xlink attribute in the XMLNS namespace whose
* value is not the XLink Namespace, that is a parse error. */
}
public function save() {
$this->dom->normalize();
if (!$this->fragment) {
return $this->dom;
} else {
if ($this->root) {
return $this->root->childNodes;
} else {
return $this->dom->childNodes;
}
}
}
}

File diff suppressed because one or more lines are too long

22
thirdparty/html5lib/LICENSE vendored Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2006-2011 The Authors
Contributors:
James Graham - jg307@cam.ac.uk
Anne van Kesteren - annevankesteren@gmail.com
Lachlan Hunt - lachlan.hunt@lachy.id.au
Matt McDonald - kanashii@kanashii.ca
Sam Ruby - rubys@intertwingly.net
Ian Hickson (Google) - ian@hixie.ch
Thomas Broyer - t.broyer@ltgt.net
Jacques Distler - distler@golem.ph.utexas.edu
Henri Sivonen - hsivonen@iki.fi
Adam Barth - abarth@webkit.org
Eric Seidel - eric@webkit.org
The Mozilla Foundation (contributions from Henri Sivonen since 2008)
David Flanagan (Mozilla) - dflanagan@mozilla.com
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

47
thirdparty/html5lib/README vendored Normal file
View File

@ -0,0 +1,47 @@
html5lib - php flavour
This is an implementation of the tokenization and tree-building parts
of the HTML5 specification in PHP. Potential uses of this library
can be found in web-scrapers and HTML filters.
Warning: This is a pre-alpha release, and as such, certain parts of
this code are not up-to-snuff (e.g. error reporting and performance).
However, the code is very close to spec and passes 100% of tests
not related to parse errors. Nevertheless, expect to have to update
your code on the next upgrade.
Usage notes:
<?php
require_once '/path/to/HTML5/Parser.php';
$dom = HTML5_Parser::parse('<html><body>...');
$nodelist = HTML5_Parser::parseFragment('<b>Boo</b><br>');
$nodelist = HTML5_Parser::parseFragment('<td>Bar</td>', 'table');
Documentation:
HTML5_Parser::parse($text)
$text : HTML to parse
return : DOMDocument of parsed document
HTML5_Parser::parseFragment($text, $context)
$text : HTML to parse
$context : String name of context element
return : DOMDocument of parsed document
Developer notes:
* To setup unit tests, you need to add a small stub file test-settings.php
that contains $simpletest_location = 'path/to/simpletest/'; This needs to
be version 1.1 (or, until that is released, SVN trunk) of SimpleTest.
* We don't want to ultimately use PHP's DOM because it is not tolerant
of certain types of errors that HTML 5 allows (for example, an element
"foo@bar"). But the current implementation uses it, since it's easy.
Eventually, this html5lib implementation will get a version of SimpleTree;
and may possibly start using that by default.
vim: et sw=4 sts=4

View File

@ -38,7 +38,20 @@
ed.addCommand('ssmedia', function(ed) {
jQuery('#' + this.id).entwine('ss').openMediaDialog();
});
// Replace the mceAdvLink and mceLink commands with the sslink command, and
// the mceAdvImage and mceImage commands with the ssmedia command
ed.onBeforeExecCommand.add(function(ed, cmd, ui, val, a){
if (cmd == 'mceAdvLink' || cmd == 'mceLink'){
ed.execCommand('sslink', ui, val, a);
a.terminate = true;
}
else if (cmd == 'mceAdvImage' || cmd == 'mceImage'){
ed.execCommand('ssmedia', ui, val, a);
a.terminate = true;
}
});
// Disable link button when no link is selected
ed.onNodeChange.add(function(ed, cm, n, co) {
cm.setDisabled('sslink', co && n.nodeName != 'A');

View File

@ -0,0 +1,5 @@
tinyMCE.addI18n('nl.tinymce_ssbuttons', {
insertlink: 'Link toevoegen',
insertmedia: 'Media toevoegen',
insertflash: 'Flash Object toevoegen'
});

View File

@ -0,0 +1,3 @@
tinyMCE.addI18n('nl.tinymce_ssmacron', {
insertmacron: 'Een macron toevoegen'
});

View File

@ -246,9 +246,10 @@ class Requirements {
*
* @param string $combinedFileName
* @param array $files
* @param string $media
*/
public static function combine_files($combinedFileName, $files) {
self::backend()->combine_files($combinedFileName, $files);
public static function combine_files($combinedFileName, $files, $media = null) {
self::backend()->combine_files($combinedFileName, $files, $media);
}
/**
@ -871,8 +872,9 @@ class Requirements_Backend {
* @param string $combinedFileName Filename of the combined file (will be stored in {@link Director::baseFolder()}
* by default)
* @param array $files Array of filenames relative to the webroot
* @param string $media Comma-separated list of media-types (e.g. "screen,projector").
*/
public function combine_files($combinedFileName, $files) {
public function combine_files($combinedFileName, $files, $media = null) {
// duplicate check
foreach($this->combine_files as $_combinedFileName => $_files) {
$duplicates = array_intersect($_files, $files);
@ -889,7 +891,7 @@ class Requirements_Backend {
if (isset($file['type']) && in_array($file['type'], array('css', 'javascript', 'js'))) {
switch ($file['type']) {
case 'css':
$this->css($file['path']);
$this->css($file['path'], $media);
break;
default:
$this->javascript($file['path']);
@ -899,7 +901,7 @@ class Requirements_Backend {
} elseif (isset($file[1]) && in_array($file[1], array('css', 'javascript', 'js'))) {
switch ($file[1]) {
case 'css':
$this->css($file[0]);
$this->css($file[0], $media);
break;
default:
$this->javascript($file[0]);
@ -914,7 +916,7 @@ class Requirements_Backend {
if(substr($file, -2) == 'js') {
$this->javascript($file);
} elseif(substr($file, -3) == 'css') {
$this->css($file);
$this->css($file, $media);
} else {
user_error("Requirements_Backend::combine_files(): Couldn't guess file type for file '$file', "
. "please specify by passing using an array instead.", E_USER_NOTICE);
@ -998,7 +1000,8 @@ class Requirements_Backend {
foreach($this->css as $file => $params) {
if(isset($combinerCheck[$file])) {
$newCSSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = true;
// Inherit the parameters from the last file in the combine set.
$newCSSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = $params;
$combinedFiles[$combinerCheck[$file]] = true;
} else {
$newCSSRequirements[$file] = $params;
@ -1109,7 +1112,7 @@ class Requirements_Backend {
$this->css($path.$css, $media);
}
else if ($module) {
$this->css($module.$css);
$this->css($module.$css, $media);
}
}

View File

@ -646,7 +646,8 @@ class SSTemplateParser extends Parser {
}
/* Translate: "<%t" < Entity < (Default:QuotedString)? < (!("is" "=") < "is" < Context:QuotedString)? < (InjectionVariables)? > "%>" */
/* Translate: "<%t" < Entity < (Default:QuotedString)? < (!("is" "=") < "is" < Context:QuotedString)? <
(InjectionVariables)? > "%>" */
protected $match_Translate_typestack = array('Translate');
function match_Translate ($stack = array()) {
$matchrule = "Translate"; $result = $this->construct($matchrule, $matchrule, null);
@ -1671,7 +1672,7 @@ class SSTemplateParser extends Parser {
function Require_Call(&$res, $sub) {
$res['php'] = "Requirements::".$sub['Method']['text'].'('.$sub['CallArguments']['php'].');';
}
/* CacheBlockArgument:
!( "if " | "unless " )
@ -2914,7 +2915,7 @@ class SSTemplateParser extends Parser {
function OldTTag_OldTPart(&$res, $sub) {
$res['php'] = $sub['php'];
}
/* OldSprintfTag: "<%" < "sprintf" < "(" < OldTPart < "," < CallArguments > ")" > "%>" */
protected $match_OldSprintfTag_typestack = array('OldSprintfTag');
function match_OldSprintfTag ($stack = array()) {
@ -3052,8 +3053,19 @@ class SSTemplateParser extends Parser {
}
function NamedArgument_Value(&$res, $sub) {
$res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php']
: str_replace('$$FINAL', 'XML_val', $sub['php']);
switch($sub['ArgumentMode']) {
case 'string':
$res['php'] .= $sub['php'];
break;
case 'default':
$res['php'] .= $sub['string_php'];
break;
default:
$res['php'] .= str_replace('$$FINAL', 'obj', $sub['php']) . '->self()';
break;
}
}
/* Include: "<%" < "include" < Template:Word < (NamedArgument ( < "," < NamedArgument )*)? > "%>" */
@ -3642,7 +3654,8 @@ class SSTemplateParser extends Parser {
$method = 'OpenBlock_Handle_'.$blockname;
if (method_exists($this, $method)) $res['php'] = $this->$method($res);
else {
throw new SSTemplateParseException('Unknown open block "'.$blockname.'" encountered. Perhaps you missed the closing tag or have mis-spelled it?', $this);
throw new SSTemplateParseException('Unknown open block "'.$blockname.'" encountered. Perhaps you missed ' .
' the closing tag or have mis-spelled it?', $this);
}
}
@ -3711,7 +3724,8 @@ class SSTemplateParser extends Parser {
function MismatchedEndBlock__finalise(&$res) {
$blockname = $res['Word']['text'];
throw new SSTemplateParseException('Unexpected close tag end_'.$blockname.' encountered. Perhaps you have mis-nested blocks, or have mis-spelled a tag?', $this);
throw new SSTemplateParseException('Unexpected close tag end_' . $blockname .
' encountered. Perhaps you have mis-nested blocks, or have mis-spelled a tag?', $this);
}
/* MalformedOpenTag: '<%' < !NotBlockTag Tag:Word !( ( [ :BlockArguments ] )? > '%>' ) */
@ -3796,7 +3810,8 @@ class SSTemplateParser extends Parser {
function MalformedOpenTag__finalise(&$res) {
$tag = $res['Tag']['text'];
throw new SSTemplateParseException("Malformed opening block tag $tag. Perhaps you have tried to use operators?", $this);
throw new SSTemplateParseException("Malformed opening block tag $tag. Perhaps you have tried to use operators?"
, $this);
}
/* MalformedCloseTag: '<%' < Tag:('end_' :Word ) !( > '%>' ) */
@ -3860,7 +3875,8 @@ class SSTemplateParser extends Parser {
function MalformedCloseTag__finalise(&$res) {
$tag = $res['Tag']['text'];
throw new SSTemplateParseException("Malformed closing block tag $tag. Perhaps you have tried to pass an argument to one?", $this);
throw new SSTemplateParseException("Malformed closing block tag $tag. Perhaps you have tried to pass an " .
"argument to one?", $this);
}
/* MalformedBlock: MalformedOpenTag | MalformedCloseTag */
@ -4448,10 +4464,12 @@ class SSTemplateParser extends Parser {
$text = stripslashes($text);
$text = addcslashes($text, '\'\\');
// TODO: This is pretty ugly & gets applied on all files not just html. I wonder if we can make this non-dynamically calculated
// TODO: This is pretty ugly & gets applied on all files not just html. I wonder if we can make this
// non-dynamically calculated
$text = preg_replace(
'/href\s*\=\s*\"\#/',
'href="\' . (SSViewer::$options[\'rewriteHashlinks\'] ? strip_tags( $_SERVER[\'REQUEST_URI\'] ) : "") . \'#',
'href="\' . (SSViewer::$options[\'rewriteHashlinks\'] ? strip_tags( $_SERVER[\'REQUEST_URI\'] ) : "") .
\'#',
$text
);
@ -4467,10 +4485,10 @@ class SSTemplateParser extends Parser {
*
* @static
* @throws SSTemplateParseException
* @param $string - The source of the template
* @param string $templateName - The name of the template, normally the filename the template source was loaded from
* @param bool $includeDebuggingComments - True is debugging comments should be included in the output
* @return mixed|string - The php that, when executed (via include or exec) will behave as per the template source
* @param $string The source of the template
* @param string $templateName The name of the template, normally the filename the template source was loaded from
* @param bool $includeDebuggingComments True is debugging comments should be included in the output
* @return mixed|string The php that, when executed (via include or exec) will behave as per the template source
*/
static function compileString($string, $templateName = "", $includeDebuggingComments=false) {
if (!trim($string)) {
@ -4481,7 +4499,8 @@ class SSTemplateParser extends Parser {
$parser = new SSTemplateParser($string);
$parser->includeDebuggingComments = $includeDebuggingComments;
// Ignore UTF8 BOM at begining of string. TODO: Confirm this is needed, make sure SSViewer handles UTF (and other encodings) properly
// Ignore UTF8 BOM at begining of string. TODO: Confirm this is needed, make sure SSViewer handles UTF
// (and other encodings) properly
if(substr($string, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) $parser->pos = 3;
// Match the source against the parser
@ -4494,12 +4513,14 @@ class SSTemplateParser extends Parser {
// Include top level debugging comments if desired
if($includeDebuggingComments && $templateName && stripos($code, "<?xml") === false) {
// If this template is a full HTML page, then put the comments just inside the HTML tag to prevent any IE glitches
// If this template is a full HTML page, then put the comments just inside the HTML tag to prevent any IE
// glitches
if(stripos($code, "<html") !== false) {
$code = preg_replace('/(<html[^>]*>)/i', "\\1<!-- template $templateName -->", $code);
$code = preg_replace('/(<\/html[^>]*>)/i', "<!-- end template $templateName -->\\1", $code);
} else {
$code = str_replace('<?php' . PHP_EOL, '<?php' . PHP_EOL . '$val .= \'<!-- template ' . $templateName . ' -->\';' . "\n", $code);
$code = str_replace('<?php' . PHP_EOL, '<?php' . PHP_EOL . '$val .= \'<!-- template ' . $templateName .
' -->\';' . "\n", $code);
$code .= "\n" . '$val .= \'<!-- end template ' . $templateName . ' -->\';';
}
}

View File

@ -662,8 +662,19 @@ class SSTemplateParser extends Parser {
}
function NamedArgument_Value(&$res, $sub) {
$res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php']
: str_replace('$$FINAL', 'XML_val', $sub['php']);
switch($sub['ArgumentMode']) {
case 'string':
$res['php'] .= $sub['php'];
break;
case 'default':
$res['php'] .= $sub['string_php'];
break;
default:
$res['php'] .= str_replace('$$FINAL', 'obj', $sub['php']) . '->self()';
break;
}
}
/*!*

View File

@ -96,7 +96,19 @@ class SSViewer_Scope {
$this->upIndex, $this->currentIndex);
return $this;
}
/**
* Gets the current object and resets the scope.
*
* @return object
*/
public function self() {
$result = $this->itemIterator ? $this->itemIterator->current() : $this->item;
$this->resetLocalScope();
return $result;
}
public function pushScope(){
$newLocalIndex = count($this->itemStack)-1;
@ -821,7 +833,7 @@ class SSViewer {
* @return string - The result of executing the template
*/
protected function includeGeneratedTemplate($cacheFile, $item, $overlay, $underlay) {
if(isset($_GET['showtemplate']) && $_GET['showtemplate']) {
if(isset($_GET['showtemplate']) && $_GET['showtemplate'] && Permission::check('ADMIN')) {
$lines = file($cacheFile);
echo "<h2>Template: $cacheFile</h2>";
echo "<pre>";