dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxContent_seo.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxContent_admin;
3
4use dbx\dbxContent\dbxContentHome;
5use dbx\dbxContent\dbxContentLng;
6use dbx\dbxContent\dbxContentLngSync;
7use dbx\dbxContent\dbxContentPageCache;
8use dbx\dbxContent\dbxContentPermalinkIndex;
9use dbx\dbxContent\dbxContentRenderer;
10
11require_once dirname(__DIR__, 2) . '/dbxContent/include/dbxContent_bootstrap_sync.php';
12
14
15 private const ACTION_TOKEN_SCOPE = 'dbxContent_admin.actions';
16
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';
20
21 private $dd_media = 'dbxMedia';
22 private $seoTexts = null;
23
30 private function seo_texts() {
31 if ($this->seoTexts) {
32 return $this->seoTexts;
33 }
34 dbx()->get_system_obj('dbxForm', 'use');
35 $texts = new \dbxForm();
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);
40 $this->seoTexts = $texts;
41 return $this->seoTexts;
42 }
43
44 private function seo_js_messages(): string {
45 $texts = $this->seo_texts();
46 $keys = array(
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',
51 'og_removed_pending',
52 );
53 $messages = array();
54 foreach ($keys as $key) {
55 $messages[$key] = $texts->get_fd_message($key);
56 }
57 return json_encode($messages, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
58 }
59
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(
67 'ok' => 0,
68 'success' => false,
69 'msg' => $texts->get_fd_message('security_token_error'),
70 ));
71 return '';
72 }
73
74 switch ($action) {
75 case 'seo_page':
76 $this->page_json();
77 return '';
78 case 'seo_save':
79 $this->save_json();
80 return '';
81 case 'seo':
82 default:
84 return $this->render_admin();
85 }
86 }
87
88 private function apply_lng_context(): void {
89 $lng = strtolower(trim((string) dbx()->get_request_var('dbx_lng', '')));
90 if ($lng === '') {
91 return;
92 }
93
94 $allowed = dbxContentLngSync::accessibleLngs();
95 if (!in_array($lng, $allowed, true)) {
96 return;
97 }
98
99 dbx()->set_system_var('dbx_lng', $lng);
100 dbx()->set_remember_var('dbx_lng', $lng, 'dbx');
101 }
102
103 private function seo_json_response(array $data): void {
104 dbx()->json_response($data, true);
105 }
106
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);
111 }
112 if (in_array((string)$action, array('seo_save', 'cms_media', 'cms_upload', 'cms_external_video'), true)) {
113 $url .= '&dbx_token=' . rawurlencode(dbx()->action_token(self::ACTION_TOKEN_SCOPE));
114 }
115 return $url;
116 }
117
118 private function check_action_token(string $action): bool {
119 $token = (string)dbx()->get_modul_var('dbx_token', '', 'varchar|max=128');
120 if (dbx()->check_action_token(self::ACTION_TOKEN_SCOPE, $token)) {
121 return true;
122 }
123 dbx()->sys_msg(
124 'security',
125 'dbxContent_admin',
126 $action,
127 'SEO-Aktion ohne gueltigen Token abgewiesen',
128 'ip=' . (string)($_SERVER['REMOTE_ADDR'] ?? '')
129 );
130 return false;
131 }
132
133 private function connect_db($db) {
134 if (!is_object($db)) {
135 return;
136 }
137 $db->connect_db_server('dbx|dbxContent.db3');
138 $db->connect_db_server('dbx|dbxMedia.db3');
139 }
140
141 private function request_json() {
142 $raw = file_get_contents('php://input');
143 if (!is_string($raw) || trim($raw) === '') {
144 return $_POST;
145 }
146 $data = json_decode($raw, true);
147 return is_array($data) ? $data : $_POST;
148 }
149
150 private function meta_robots_values() {
151 $texts = $this->seo_texts();
152 return array(
153 'index,follow' => $texts->get_fd_message('robots_default'),
154 'noindex,follow' => 'noindex, follow',
155 'index,nofollow' => 'index, nofollow',
156 'noindex,nofollow' => 'noindex, nofollow',
157 );
158 }
159
160 private function render_admin() {
161 $texts = $this->seo_texts();
162 $db = dbx()->get_system_obj('dbxDB');
163 $this->ensure_cms_schema($db);
164
165 $pages = $db->select(
166 dbxContentLng::ddContent(),
167 '',
168 'id,title,permalink,activ',
169 'title',
170 'ASC',
171 '',
172 0,
173 0,
174 0
175 );
176 if (!is_array($pages)) {
177 $pages = array();
178 }
179
180 $cid = (int)dbx()->get_modul_var('cid', 0, 'int');
181 if ($cid <= 0 && !empty($pages[0]['id'])) {
182 $cid = (int)$pages[0]['id'];
183 }
184
185 $options = '';
186 foreach ($pages as $page) {
187 if (!is_array($page)) continue;
188 $id = (int)($page['id'] ?? 0);
189 if ($id <= 0) continue;
190 $label = trim((string)($page['title'] ?? ''));
191 if ($label === '') {
192 $label = $texts->format_fd_message('page_fallback', array('id' => $id));
193 }
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>';
198 }
199
200 $form = $this->render_seo_form();
201 $content = dbx()->get_system_obj('dbxTPL')->get_tpl('dbxContent_admin|seo-admin', array(
202 'page_options' => $options,
203 'cid' => $cid,
204 'seo_form' => $form,
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'),
224 'cms-media-upload',
225 $this->base_url('cms_external_video'),
226 'cms-external-video'
227 ),
228 ));
229
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'),
233 'content' => $content,
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>',
235 ));
236 }
237
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(
246 'id' => 0,
247 'keywords' => '',
248 'meta_robots' => 'index,follow',
249 'seo_title' => '',
250 'seo_image_id' => 0,
251 );
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'));
258 return $form->run();
259 }
260
261 private function page_json() {
262 $cid = (int)dbx()->get_modul_var('id', 0, 'int');
263 if ($cid <= 0) {
264 $cid = (int)dbx()->get_modul_var('cid', 0, 'int');
265 }
266 if ($cid <= 0) {
267 $this->seo_json_response(array('ok' => 0, 'error' => $this->seo_texts()->get_fd_message('page_id_missing')));
268 }
269
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')));
276 }
277
278 $this->seo_json_response(array(
279 'ok' => 1,
280 'row' => $row,
281 'seo_preview_media' => $this->seo_preview_media($db, $row),
282 ));
283 }
284
285 private function save_json() {
286 $payload = $this->request_json();
287 $id = (int)($payload['id'] ?? 0);
288 if ($id <= 0) {
289 $this->seo_json_response(array('ok' => 0, 'error' => $this->seo_texts()->get_fd_message('page_id_missing')));
290 }
291
292 $db = dbx()->get_system_obj('dbxDB');
293 $this->connect_db($db);
294 $this->ensure_cms_schema($db);
295
296 $data = array(
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)),
301 );
302
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')));
305 }
306
307 dbxContentLngSync::afterPageSave($db, $id, false);
308 dbxContentPageCache::invalidateContent($id);
309 dbxContentPageCache::invalidateAllMenus();
310 $this->sync_page_meta($db, $id);
311
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(
314 'ok' => 1,
315 'row' => is_array($row) ? $row : array(),
316 'seo_preview_media' => $this->seo_preview_media($db, is_array($row) ? $row : array()),
317 ));
318 }
319
320 private function sync_page_meta($db, int $cid) {
321 $cid = (int)$cid;
322 if ($cid <= 0) {
323 return;
324 }
325
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)) {
328 return;
329 }
330
331 $renderer = new dbxContentRenderer();
332 $rights = $renderer->getPublicFolderRights((int)($rec['folder'] ?? 0));
333 dbxContentPageCache::writePageMeta($cid, array(
334 'cid' => $cid,
335 'permalink' => (string)($rec['permalink'] ?? ''),
336 'rights' => $rights,
337 'activ' => (int)($rec['activ'] ?? 1),
338 'saved_at' => date('c'),
339 'seo' => dbxContentRenderer::seoMetaFromRecord($rec),
340 ));
341
342 $permalink = trim((string)($rec['permalink'] ?? ''));
343 if ($permalink !== '' && (int)($rec['activ'] ?? 0) === 1) {
344 dbxContentPermalinkIndex::upsertPage($cid, $permalink, $rights, 1, dbxContentLng::current());
345 }
346
347 if (class_exists('dbx\\dbxContent\\dbxContentSitemap')) {
349 }
350 }
351
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);
360 }
361
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 : '';
365 $thumb = $url;
366 if ($id > 0 && !empty($media['thumb_file_path'])) {
367 $thumb .= '&dbx_thumb=1';
368 }
369 return array(
370 'id' => $id,
371 'title' => (string)($media['title'] ?? ''),
372 'mime' => (string)($media['mime'] ?? ''),
373 'media_type' => (string)($media['media_type'] ?? ''),
374 'url' => $url,
375 'thumb_url' => $thumb,
376 'preview_url' => $thumb,
377 );
378 }
379
380 private function clean_text($value, int $max = 0) {
381 $value = trim((string)$value);
382 if ($max > 0 && strlen($value) > $max) {
383 $value = substr($value, 0, $max);
384 }
385 return $value;
386 }
387
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')) {
391 $cms->ensure_schema($db);
392 }
393 }
394
395 public function ensure_default_og_image() {
396 $db = dbx()->get_system_obj('dbxDB');
397 if (!is_object($db)) return 0;
398
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);
403 if ($media_id > 0) {
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);
407 return $media_id;
408 }
409 }
410
411 if (!is_file(self::OG_SOURCE)) {
412 return 0;
413 }
414
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);
419 }
420
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);
424 }
425 if (!is_file($target_file)) {
426 return 0;
427 }
428
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);
434 } else {
435 $size = (int)@filesize($target_file);
436 $info = @getimagesize($target_file);
437 $insert = array(
438 'active' => 1,
439 'content_id' => 0,
440 'folder_id' => 0,
441 'slot' => 'seo',
442 'usage' => 'seo',
443 'sorter' => 1,
444 'template' => '',
445 'title' => 'dbXapp OG',
446 'alt' => 'dbXapp',
447 'file_name' => 'dbxapp-og.png',
448 'file_path' => $rel,
449 'mime' => 'image/png',
450 'size' => $size,
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',
456 );
457 if ($db->insert($this->dd_media, $insert) !== 1) {
458 return 0;
459 }
460 $media_id = (int)$db->get_insert_id();
461 }
462
463 if ($media_id <= 0) {
464 return 0;
465 }
466
467 $config = dbx()->get_config('dbxContent');
468 if (!is_array($config)) $config = array();
469 $config[self::CONFIG_OG_MEDIA_ID] = $media_id;
470 dbx()->set_config('dbxContent', $config);
471
472 $this->assign_home_og_image($db, $media_id);
473 return $media_id;
474 }
475
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();
480 }
481 if ($home_cid <= 0) {
482 return;
483 }
484
485 $row = $db->select1(dbxContentLng::ddContent(), $home_cid, 'seo_image_id', 0);
486 if (!is_array($row)) {
487 return;
488 }
489 if ((int)($row['seo_image_id'] ?? 0) > 0) {
490 return;
491 }
492
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);
497 }
498}
499
500?>
$messages
Definition config.fd.php:2
dbx()
Liefert die zentrale dbXapp-API als Singleton.
Definition dbxApi.php:2805
esc($value)
Escaped einen Wert fuer HTML-Text und HTML-Attribute.
Definition dbxApi.php:2310
get_file_dir()
Liefert das files/-Verzeichnis der Installation.
Definition dbxApi.php:1524
action_token(string $scope='global')
Liefert ein sessiongebundenes Token fuer zustandsaendernde Link-Aktionen.
Definition dbxApi.php:1189
json_response(array $data, bool $withRuntime=false)
Beendet den Request mit JSON-Ausgabe.
Definition dbxApi.php:1647
check_action_token(string $scope='global', string $token='')
Prueft ein sessiongebundenes Aktions-Token.
Definition dbxApi.php:1206
$_SERVER['REQUEST_METHOD']
if($resolved !==$expectedBase . 'files/test/') $config
foreach(array('bootstrapRowColumns', 'setBootstrapColumnLayout', 'addBootstrapColumn', 'dissolveBootstrapColumns',) as $function) $keys
DBX schema administration.
if($cinematic===''||!str_contains($cinematic, 'data-dbx-cinema')) $page
for( $step=0;$step< 1000;$step++) if(($schema['status'] ?? '') !=='finished') $media