15 private const ACTION_TOKEN_SCOPE =
'dbxContent_admin.actions';
17 private const OG_SOURCE = __DIR__ .
'/../files/og/dbxapp-og.png';
18 private const OG_MEDIA_REL =
'media/dbxContent/seo/dbxapp-og.png';
19 private const CONFIG_OG_MEDIA_ID =
'default_seo_image_id';
21 private $dd_media =
'dbxMedia';
22 private $seoTexts =
null;
30 private function seo_texts() {
31 if ($this->seoTexts) {
32 return $this->seoTexts;
34 dbx()->get_system_obj(
'dbxForm',
'use');
36 $texts->init(
'seo-page-texts');
37 $texts->_fd =
'dbxContent_admin|seo-page';
38 $texts->load_fd_messages();
39 $texts->set_form_help_enabled(
false);
41 return $this->seoTexts;
44 private function seo_js_messages():
string {
45 $texts = $this->seo_texts();
47 'save_error',
'og_empty',
'og_loading',
'og_not_image',
48 'load_start',
'load_error',
'no_page_selected',
'save_start',
49 'seo_saved',
'no_media_images',
'picker_title',
50 'media_api_missing',
'og_selected_pending',
'media_load_error',
54 foreach (
$keys as $key) {
57 return json_encode(
$messages, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
60 public function run($action =
'seo') {
61 $this->apply_lng_context();
62 $texts = $this->seo_texts();
63 $action = (string)$action;
64 if ($action ===
'seo_save' && !$this->check_action_token($action)) {
65 http_response_code(403);
66 $this->seo_json_response(array(
69 'msg' =>
$texts->get_fd_message(
'security_token_error'),
84 return $this->render_admin();
88 private function apply_lng_context(): void {
89 $lng = strtolower(trim((string)
dbx()->get_request_var(
'dbx_lng',
'')));
94 $allowed = dbxContentLngSync::accessibleLngs();
99 dbx()->set_system_var(
'dbx_lng',
$lng);
100 dbx()->set_remember_var(
'dbx_lng',
$lng,
'dbx');
103 private function seo_json_response(array $data): void {
107 private function base_url($action, $params = array()) {
108 $url =
'?dbx_modul=dbxContent_admin&dbx_run1=' . rawurlencode((
string)$action);
109 foreach ($params as $key =>
$value) {
110 $url .=
'&' . rawurlencode((
string)$key) .
'=' . rawurlencode((
string)
$value);
112 if (in_array((
string)$action, array(
'seo_save',
'cms_media',
'cms_upload',
'cms_external_video'),
true)) {
119 $token = (string)
dbx()->get_modul_var(
'dbx_token',
'',
'varchar|max=128');
127 'SEO-Aktion ohne gueltigen Token abgewiesen',
128 'ip=' . (
string)(
$_SERVER[
'REMOTE_ADDR'] ??
'')
133 private function connect_db(
$db) {
134 if (!is_object(
$db)) {
137 $db->connect_db_server(
'dbx|dbxContent.db3');
138 $db->connect_db_server(
'dbx|dbxMedia.db3');
141 private function request_json() {
142 $raw = file_get_contents(
'php://input');
143 if (!is_string($raw) || trim($raw) ===
'') {
146 $data = json_decode($raw,
true);
147 return is_array($data) ? $data :
$_POST;
150 private function meta_robots_values() {
151 $texts = $this->seo_texts();
153 'index,follow' =>
$texts->get_fd_message(
'robots_default'),
154 'noindex,follow' =>
'noindex, follow',
155 'index,nofollow' =>
'index, nofollow',
156 'noindex,nofollow' =>
'noindex, nofollow',
160 private function render_admin() {
161 $texts = $this->seo_texts();
162 $db =
dbx()->get_system_obj(
'dbxDB');
163 $this->ensure_cms_schema(
$db);
166 dbxContentLng::ddContent(),
168 'id,title,permalink,activ',
180 $cid = (int)
dbx()->get_modul_var(
'cid', 0,
'int');
181 if ($cid <= 0 && !empty(
$pages[0][
'id'])) {
182 $cid = (int)
$pages[0][
'id'];
187 if (!is_array(
$page))
continue;
188 $id = (int)(
$page[
'id'] ?? 0);
189 if ($id <= 0)
continue;
190 $label = trim((
string)(
$page[
'title'] ??
''));
192 $label =
$texts->format_fd_message(
'page_fallback', array(
'id' => $id));
194 $permalink = trim((
string)(
$page[
'permalink'] ??
''));
195 if ($permalink !==
'') $label .=
' (' . $permalink .
')';
196 $selected = ($id === $cid) ?
' selected' :
'';
197 $options .=
'<option value="' . $id .
'"' . $selected .
'>' . htmlspecialchars($label, ENT_QUOTES,
'UTF-8') .
'</option>';
200 $form = $this->render_seo_form();
201 $content =
dbx()->get_system_obj(
'dbxTPL')->get_tpl(
'dbxContent_admin|seo-admin', array(
205 'seo_messages' =>
dbx()->
esc($this->seo_js_messages()),
206 'page_label' =>
$texts->get_fd_message(
'page_label'),
207 'page_description_hint' =>
$texts->get_fd_message(
'page_description_hint'),
208 'save_button' =>
$texts->get_fd_message(
'save_button'),
209 'label_og_image' =>
$texts->get_fd_message(
'label_og_image'),
210 'og_empty' =>
$texts->get_fd_message(
'og_empty'),
211 'og_select_title' =>
$texts->get_fd_message(
'og_select_title'),
212 'og_select' =>
$texts->get_fd_message(
'og_select'),
213 'og_remove_title' =>
$texts->get_fd_message(
'og_remove_title'),
214 'og_remove' =>
$texts->get_fd_message(
'og_remove'),
215 'page_url' =>
dbx()->
esc($this->base_url(
'seo_page')),
216 'save_url' =>
dbx()->
esc($this->base_url(
'seo_save')),
217 'media_url' =>
dbx()->
esc($this->base_url(
'cms_media')),
218 'media_folders_url' =>
dbx()->
esc($this->base_url(
'cms_media_folders')),
219 'upload_url' =>
dbx()->
esc($this->base_url(
'cms_upload')),
220 'external_video_url' =>
dbx()->
esc($this->base_url(
'cms_external_video')),
221 'upload_max_bytes' => (
string)(16 * 1024 * 1024),
222 'media_browser_forms' =>
dbx()->get_include_obj(
'dbxContentMediaForms',
'dbxContent_admin')->renderTemplates(
223 $this->base_url(
'cms_upload'),
225 $this->base_url(
'cms_external_video'),
230 return dbx()->get_system_obj(
'dbxTPL')->get_tpl(
'dbxContent_admin|content-admin-section', array(
231 'title' =>
$texts->get_fd_message(
'bar_title'),
232 'subtitle' =>
$texts->get_fd_message(
'bar_subtitle'),
234 'bar_actions' =>
'<a class="btn btn-outline-primary btn-sm" href="?dbx_modul=dbxContent_admin&dbx_run1=content&dbx_run2=edit"><i class="bi bi-pencil-square"></i><span>' .
$texts->get_fd_message(
'cms_action') .
'</span></a>',
238 private function render_seo_form() {
239 $form =
dbx()->get_system_obj(
'dbxForm');
240 $form->init(
'seo-page',
'seo-admin-form');
241 $form->_fd =
'dbxContent_admin|seo-page';
242 $form->load_fd_messages();
243 $form->set_form_help_enabled(
false);
244 $form->_dd = dbxContentLng::ddContent();
245 $form->_data = array(
248 'meta_robots' =>
'index,follow',
252 $texts = $this->seo_texts();
253 $form->add_fld(
'id',
'dbxContent_admin|cms-field-hidden', data: array(
'cms_field' =>
'id',
'seo_field' =>
'id'));
254 $form->add_fld(
'keywords',
'dbxContent_admin|cms-field-text', label:
$texts->get_fd_message(
'label_keywords'), rules:
'varchar', data: array(
'cms_field' =>
'keywords',
'seo_field' =>
'keywords'), placeholder:
$texts->get_fd_message(
'placeholder_keywords'));
255 $form->add_fld(
'meta_robots',
'dbxContent_admin|cms-field-select', label:
$texts->get_fd_message(
'label_robots'), rules:
'varchar', data: array(
'cms_field' =>
'meta_robots',
'seo_field' =>
'meta_robots'), options: $this->meta_robots_values());
256 $form->add_fld(
'seo_title',
'dbxContent_admin|cms-field-text', label:
$texts->get_fd_message(
'label_seo_title'), rules:
'varchar', data: array(
'cms_field' =>
'seo_title',
'seo_field' =>
'seo_title'), placeholder:
$texts->get_fd_message(
'placeholder_seo_title'));
257 $form->add_fld(
'seo_image_id',
'dbxContent_admin|cms-field-hidden', rules:
'int', data: array(
'cms_field' =>
'seo_image_id',
'seo_field' =>
'seo_image_id'));
261 private function page_json() {
262 $cid = (int)
dbx()->get_modul_var(
'id', 0,
'int');
264 $cid = (int)
dbx()->get_modul_var(
'cid', 0,
'int');
267 $this->seo_json_response(array(
'ok' => 0,
'error' => $this->seo_texts()->get_fd_message(
'page_id_missing')));
270 $db =
dbx()->get_system_obj(
'dbxDB');
271 $this->connect_db(
$db);
272 $this->ensure_cms_schema(
$db);
273 $row =
$db->select1(dbxContentLng::ddContent(), $cid,
'id,title,permalink,keywords,meta_robots,seo_title,seo_image_id,description', 0);
274 if (!is_array($row) || (
int)($row[
'id'] ?? 0) <= 0) {
275 $this->seo_json_response(array(
'ok' => 0,
'error' => $this->seo_texts()->get_fd_message(
'page_not_found')));
278 $this->seo_json_response(array(
281 'seo_preview_media' => $this->seo_preview_media(
$db, $row),
285 private function save_json() {
286 $payload = $this->request_json();
287 $id = (int)($payload[
'id'] ?? 0);
289 $this->seo_json_response(array(
'ok' => 0,
'error' => $this->seo_texts()->get_fd_message(
'page_id_missing')));
292 $db =
dbx()->get_system_obj(
'dbxDB');
293 $this->connect_db(
$db);
294 $this->ensure_cms_schema(
$db);
297 'keywords' => $this->clean_text($payload[
'keywords'] ??
'', 254),
298 'meta_robots' => $this->clean_text($payload[
'meta_robots'] ??
'index,follow', 32),
299 'seo_title' => $this->clean_text($payload[
'seo_title'] ??
'', 254),
300 'seo_image_id' => max(0, (
int)($payload[
'seo_image_id'] ?? 0)),
303 if (
$db->update(dbxContentLng::ddContent(), $data, $id) !== 1) {
304 $this->seo_json_response(array(
'ok' => 0,
'error' => $this->seo_texts()->get_fd_message(
'save_error')));
307 dbxContentLngSync::afterPageSave(
$db, $id,
false);
308 dbxContentPageCache::invalidateContent($id);
309 dbxContentPageCache::invalidateAllMenus();
310 $this->sync_page_meta(
$db, $id);
312 $row =
$db->select1(dbxContentLng::ddContent(), $id,
'id,title,permalink,keywords,meta_robots,seo_title,seo_image_id,description', 0);
313 $this->seo_json_response(array(
315 'row' => is_array($row) ? $row : array(),
316 'seo_preview_media' => $this->seo_preview_media(
$db, is_array($row) ? $row : array()),
320 private function sync_page_meta(
$db,
int $cid) {
326 $rec =
$db->select1(dbxContentLng::ddContent(), $cid,
'permalink,activ,folder,title,seo_title,description,keywords,meta_robots,seo_image_id,update_date,lng_uid', 0);
327 if (!is_array($rec)) {
332 $rights =
$renderer->getPublicFolderRights((
int)($rec[
'folder'] ?? 0));
333 dbxContentPageCache::writePageMeta($cid, array(
335 'permalink' => (
string)($rec[
'permalink'] ??
''),
337 'activ' => (
int)($rec[
'activ'] ?? 1),
338 'saved_at' => date(
'c'),
339 'seo' => dbxContentRenderer::seoMetaFromRecord($rec),
342 $permalink = trim((
string)($rec[
'permalink'] ??
''));
343 if ($permalink !==
'' && (
int)($rec[
'activ'] ?? 0) === 1) {
344 dbxContentPermalinkIndex::upsertPage($cid, $permalink, $rights, 1, dbxContentLng::current());
347 if (class_exists(
'dbx\\dbxContent\\dbxContentSitemap')) {
352 private function seo_preview_media(
$db, $row) {
353 if (!is_array($row))
return array();
354 $seo_id = (int)($row[
'seo_image_id'] ?? 0);
355 if ($seo_id <= 0)
return array();
356 $media =
$db->select1($this->dd_media, $seo_id);
357 if (!is_array(
$media) || (
int)(
$media[
'id'] ?? 0) <= 0)
return array();
358 if ((
int)(
$media[
'active'] ?? 0) !== 1)
return array();
359 return $this->media_row_json(
$media);
362 private function media_row_json(array
$media) {
363 $id = (int)(
$media[
'id'] ?? 0);
364 $url = $id > 0 ?
'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=' . $id :
'';
366 if ($id > 0 && !empty(
$media[
'thumb_file_path'])) {
367 $thumb .=
'&dbx_thumb=1';
371 'title' => (
string)(
$media[
'title'] ??
''),
372 'mime' => (
string)(
$media[
'mime'] ??
''),
373 'media_type' => (
string)(
$media[
'media_type'] ??
''),
375 'thumb_url' => $thumb,
376 'preview_url' => $thumb,
380 private function clean_text(
$value,
int $max = 0) {
382 if ($max > 0 && strlen(
$value) > $max) {
388 private function ensure_cms_schema(
$db) {
389 $cms =
dbx()->get_include_obj(
'dbxContent_cms');
390 if (is_object(
$cms) && method_exists(
$cms,
'ensure_schema')) {
396 $db =
dbx()->get_system_obj(
'dbxDB');
397 if (!is_object(
$db))
return 0;
399 $db->connect_db_server(
'dbx|dbxContent.db3');
400 $db->connect_db_server(
'dbx|dbxMedia.db3');
401 $this->ensure_cms_schema(
$db);
402 $media_id = (int)
dbx()->get_config(
'dbxContent', self::CONFIG_OG_MEDIA_ID);
404 $exists =
$db->select1($this->dd_media, $media_id,
'id,active', 0);
405 if (is_array($exists) && (
int)($exists[
'id'] ?? 0) > 0 && (
int)($exists[
'active'] ?? 0) === 1) {
406 $this->assign_home_og_image(
$db, $media_id);
411 if (!is_file(self::OG_SOURCE)) {
415 $target_dir = rtrim(
dbx()->
get_file_dir(),
'/\\') .
'/media/dbxContent/seo/';
416 $target_dir =
dbx()->os_path($target_dir);
417 if (!is_dir($target_dir)) {
418 @mkdir($target_dir, 0775,
true);
421 $target_file = $target_dir .
'dbxapp-og.png';
422 if (!is_file($target_file) || filemtime(self::OG_SOURCE) > filemtime($target_file)) {
423 @copy(self::OG_SOURCE, $target_file);
425 if (!is_file($target_file)) {
429 $rel = self::OG_MEDIA_REL;
430 $existing =
$db->select1($this->dd_media, array(
'file_path' => $rel),
'id,active', 0);
431 if (is_array($existing) && (
int)($existing[
'id'] ?? 0) > 0) {
432 $media_id = (int)$existing[
'id'];
433 $db->update($this->dd_media, array(
'active' => 1,
'title' =>
'dbXapp OG',
'alt' =>
'dbXapp'), $media_id);
435 $size = (int)@filesize($target_file);
436 $info = @getimagesize($target_file);
445 'title' =>
'dbXapp OG',
447 'file_name' =>
'dbxapp-og.png',
449 'mime' =>
'image/png',
451 'width' => is_array($info) ? (
int)($info[0] ?? 0) : 0,
452 'height' => is_array($info) ? (
int)($info[1] ?? 0) : 0,
453 'media_type' =>
'image',
454 'storage_type' =>
'local',
455 'media_folder' =>
'dbxContent/seo',
457 if (
$db->insert($this->dd_media, $insert) !== 1) {
460 $media_id = (int)
$db->get_insert_id();
463 if ($media_id <= 0) {
469 $config[self::CONFIG_OG_MEDIA_ID] = $media_id;
472 $this->assign_home_og_image(
$db, $media_id);
476 private function assign_home_og_image(
$db,
int $media_id) {
477 $home_cid = dbxContentHome::resolveCid();
478 if ($home_cid <= 0) {
479 $home_cid = dbxContentHome::masterCid();
481 if ($home_cid <= 0) {
485 $row =
$db->select1(dbxContentLng::ddContent(), $home_cid,
'seo_image_id', 0);
486 if (!is_array($row)) {
489 if ((
int)($row[
'seo_image_id'] ?? 0) > 0) {
493 $db->update(dbxContentLng::ddContent(), array(
'seo_image_id' => $media_id), $home_cid);
494 dbxContentPageCache::invalidateContent($home_cid);
495 dbxContentPageCache::invalidateAllMenus();
496 $this->sync_page_meta(
$db, $home_cid);
foreach(array('bootstrapRowColumns', 'setBootstrapColumnLayout', 'addBootstrapColumn', 'dissolveBootstrapColumns',) as $function) $keys
for( $step=0;$step< 1000;$step++) if(($schema['status'] ?? '') !=='finished') $media