├── plugins └── .redaxo ├── pages ├── index.php └── settings.php ├── .travis.yml ├── install.php ├── package.yml ├── .github └── workflows │ └── publish-to-redaxo.yml ├── update.php ├── lang ├── pt_br.lang ├── en_gb.lang ├── sv_se.lang ├── de_de.lang └── es_es.lang ├── LICENSE ├── lib ├── command.php ├── synchronizer_item.php ├── manager.php ├── synchronizer_default.php └── synchronizer.php ├── boot.php ├── README.md └── CHANGELOG.md /plugins/.redaxo: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/index.php: -------------------------------------------------------------------------------- 1 | i18n('name')); 6 | 7 | rex_be_controller::includeCurrentPageSubPath(); 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - '7.1' 5 | 6 | cache: 7 | directories: 8 | - $HOME/.composer/cache 9 | 10 | before_install: 11 | - phpenv config-rm xdebug.ini || echo "xdebug not available" 12 | 13 | script: 14 | - composer require --dev friendsofredaxo/linter 15 | - vendor/bin/rexlint 16 | -------------------------------------------------------------------------------- /install.php: -------------------------------------------------------------------------------- 1 | hasConfig()) { 6 | $this->setConfig([ 7 | 'templates' => true, 8 | 'modules' => true, 9 | 'actions' => true, 10 | 'yform_email' => true, 11 | 'sync_frontend' => true, 12 | 'sync_backend' => true, 13 | 'rename' => true, 14 | 'dir_suffix' => true, 15 | 'prefix' => false, 16 | 'umlauts' => false, 17 | 'delete' => true, 18 | ]); 19 | } 20 | -------------------------------------------------------------------------------- /package.yml: -------------------------------------------------------------------------------- 1 | package: developer 2 | version: '3.9.3' 3 | author: Friends Of REDAXO 4 | supportpage: https://github.com/FriendsOfREDAXO/developer 5 | 6 | page: 7 | title: translate:name 8 | perm: admin 9 | icon: rex-icon fa-code 10 | subpages: 11 | settings: { title: translate:settings } 12 | readme: { title: translate:readme, subPath: README.md } 13 | 14 | console_commands: 15 | developer:sync: rex_developer_command 16 | 17 | requires: 18 | php: 19 | version: '>=5.5' 20 | extensions: [iconv] 21 | redaxo: ^5.2 22 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-redaxo.yml: -------------------------------------------------------------------------------- 1 | name: Publish release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | redaxo_publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: FriendsOfREDAXO/installer-action@v1 14 | with: 15 | myredaxo-username: ${{ secrets.MYREDAXO_USERNAME }} 16 | myredaxo-api-key: ${{ secrets.MYREDAXO_API_KEY }} 17 | description: ${{ github.event.release.body }} 18 | -------------------------------------------------------------------------------- /update.php: -------------------------------------------------------------------------------- 1 | getVersion(), '3.4.1', '<')) { 8 | rex_file::delete($this->getDataPath('actions/.rex_id_list')); 9 | rex_file::delete($this->getDataPath('modules/.rex_id_list')); 10 | rex_file::delete($this->getDataPath('templates/.rex_id_list')); 11 | } 12 | 13 | if (rex_string::versionCompare($this->getVersion(), '3.5.0', '<')) { 14 | $this->setConfig('sync_frontend', true); 15 | } 16 | 17 | if (rex_string::versionCompare($this->getVersion(), '3.6.0', '<')) { 18 | $this->setConfig('sync_backend', true); 19 | } 20 | 21 | if (rex_string::versionCompare($this->getVersion(), '3.6.0-beta2', '<')) { 22 | $this->setConfig('dir_suffix', false); 23 | } 24 | -------------------------------------------------------------------------------- /lang/pt_br.lang: -------------------------------------------------------------------------------- 1 | developer_name = Programador 2 | developer_settings = Configurações 3 | developer_templates = Sincronizar templates 4 | developer_modules = Sincronizar módulos 5 | developer_actions = Sincronizar ações 6 | developer_sync_frontend = Sincronizar no frontend (somente se registrado como administrador no backend) 7 | developer_sync_backend = Sincronizar no backend (somente se registrado como administrador) 8 | developer_rename = Atualizar nomes de arquivo e diretórios automaticamente 9 | developer_prefix = Prefixo do nome do arquivo (contem ID e nome) 10 | developer_umlauts = Manter acentos nos nomes 11 | developer_delete = Deletar item do diretório depois de deletar item no backend 12 | developer_dir = Diretório 13 | developer_save = Salvar configurações 14 | developer_saved = Configurações foram salvas 15 | developer_error = Um erro ocorreu 16 | -------------------------------------------------------------------------------- /lang/en_gb.lang: -------------------------------------------------------------------------------- 1 | developer_name = Developer 2 | 3 | developer_settings = Settings 4 | developer_templates = Synchronize templates 5 | developer_modules = Synchronize modules 6 | developer_actions = Synchronize actions 7 | developer_yform_email = YForm: Synchronize email templates 8 | developer_sync_frontend = Synchronize in frontend (only if signed in as admin in backend) 9 | developer_sync_backend = Synchronize in backend (only if signed in as admin) 10 | developer_rename = Update file and directory names automatically 11 | developer_dir_suffix = Dir names with id suffix 12 | developer_prefix = Filename prefix (contains ID and name) 13 | developer_umlauts = Keep umlauts in names (deprecated; will be removed in next major version) 14 | developer_delete = Delete item directory after deleting item in backend 15 | developer_dir = Directory 16 | developer_save = Save settings 17 | developer_saved = Settings have been saved 18 | developer_error = An error occured 19 | 20 | developer_readme = Readme 21 | -------------------------------------------------------------------------------- /lang/sv_se.lang: -------------------------------------------------------------------------------- 1 | developer_name = Developer 2 | 3 | developer_settings = Inställningar 4 | developer_templates = Synkronisera mallar 5 | developer_modules = Synkronisera moduler 6 | developer_actions = Synkronisera aktioner 7 | developer_yform_email = YForm: Synkronisera e-postmallar 8 | developer_sync_frontend = Synkronisera i frontend (endast om du är inloggad som administratör i backend) 9 | developer_sync_backend = Synkronisera i backend (endast om du är inloggad som administratör) 10 | developer_rename = Uppdatera fil- och katalognamn automatiskt 11 | developer_dir_suffix = Mappnamn med ID som suffix 12 | developer_prefix = Filnamn prefix (innehåller ID och namn) 13 | developer_umlauts = Håll umlauts i namn 14 | developer_delete = Ta bort artikelkatalog efter att du har raderat objektet i backend 15 | developer_dir = Mapp 16 | developer_save = Spara inställningarna 17 | developer_saved = Inställningarna har sparats 18 | developer_error = Det uppstod ett fel 19 | 20 | developer_readme = Hjälp 21 | -------------------------------------------------------------------------------- /lang/de_de.lang: -------------------------------------------------------------------------------- 1 | developer_name = Developer 2 | 3 | developer_settings = Einstellungen 4 | developer_templates = Templates synchronisieren 5 | developer_modules = Module synchronisieren 6 | developer_actions = Actions synchronisieren 7 | developer_yform_email = YForm: E-Mail-Templates synchronisieren 8 | developer_sync_frontend = Im Frontend synchronisieren (nur wenn als Admin im Backend eingeloggt) 9 | developer_sync_backend = Im Backend synchronisieren (nur wenn als Admin eingeloggt) 10 | developer_rename = Datei- und Ordnernamen aktuell halten 11 | developer_dir_suffix = Ordnernamen mit ID als Suffix 12 | developer_prefix = Präfix für Dateinamen (enthält ID und Name) 13 | developer_umlauts = Umlaute in Namen beibehalten (Deprecated; die Option wird in der nächsten Major-Version wegfallen und somit immer deaktiviert sein) 14 | developer_delete = Item-Ordner löschen nach dem Löschen eines Items über das Backend 15 | developer_dir = Ordner 16 | developer_save = Einstellungen speichern 17 | developer_saved = Die Einstellungen wurden gespeichert. 18 | developer_error = Es ist ein Fehler aufgetreten. 19 | 20 | developer_readme = Hilfe 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Friends Of REDAXO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lang/es_es.lang: -------------------------------------------------------------------------------- 1 | developer_name = Desarrollador 2 | 3 | developer_settings = Ajustes 4 | developer_templates = Sincronizar plantillas 5 | developer_modules = Sincronizar módulos 6 | developer_actions = Sincronizar acciones 7 | developer_yform_email = YForm: sincronizar plantillas de correo electrónico 8 | developer_sync_frontend = Sincronizar en frontend (solo iniciado sesión como administrador en back-end) 9 | developer_sync_backend = Sincronizar en el backend (solo si está conectado como administrador) 10 | developer_rename = Mantenga actualizados los nombres de archivos y carpetas 11 | developer_dir_suffix = Nombre de la carpeta con ID como sufijo 12 | developer_prefix = Prefijo de nombre de archivo (contiene ID y nombre) 13 | developer_umlauts = Mantenga umlauts en los nombres (obsoleto, la opción desaparecerá en la siguiente versión principal y por lo tanto siempre estará deshabilitada) 14 | developer_delete = Eliminar carpeta de elementos después de eliminar un elemento a través del servidor 15 | developer_dir = Directorio 16 | developer_save = Guardar configuraciones 17 | developer_saved = La configuración ha sido guardada. 18 | developer_error = Un error ha ocurrido 19 | 20 | developer_readme = Léame 21 | -------------------------------------------------------------------------------- /lib/command.php: -------------------------------------------------------------------------------- 1 | setName('developer:sync') 14 | ->setDescription('Synchronizes the developer files') 15 | ->addOption('force-db', null, InputOption::VALUE_NONE, 'Force the current status in db, files will be overridden') 16 | ->addOption('force-files', null, InputOption::VALUE_NONE, 'Force the current status in file system, db data will be overridden') 17 | ; 18 | } 19 | 20 | protected function execute(InputInterface $input, OutputInterface $output) 21 | { 22 | $io = $this->getStyle($input, $output); 23 | 24 | $io->title('Developer Sync'); 25 | 26 | $forceDb = $input->getOption('force-db'); 27 | $forceFiles = $input->getOption('force-files'); 28 | 29 | if ($forceDb && $forceFiles) { 30 | throw new InvalidArgumentException('Options --force-db and --force-files can not be used at once.'); 31 | } 32 | 33 | $force = false; 34 | if ($forceDb) { 35 | $force = rex_developer_synchronizer::FORCE_DB; 36 | } elseif ($forceFiles) { 37 | $force = rex_developer_synchronizer::FORCE_FILES; 38 | } 39 | 40 | rex_developer_manager::start($force); 41 | 42 | $io->success('Synchronized developer files.'); 43 | 44 | return 0; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /boot.php: -------------------------------------------------------------------------------- 1 | isAvailable() && rex_media_manager::getMediaType() && rex_media_manager::getMediaFile()) { 12 | return; 13 | } 14 | 15 | if ( 16 | !rex::isBackend() && $this->getConfig('sync_frontend') || 17 | rex::getUser() && rex::isBackend() && $this->getConfig('sync_backend') 18 | ) { 19 | rex_extension::register('PACKAGES_INCLUDED', function () { 20 | if (rex::isDebugMode() || ($user = rex_backend_login::createUser()) && $user->isAdmin()) { 21 | rex_developer_manager::start(); 22 | } 23 | }); 24 | } 25 | 26 | rex_extension::register('EDITOR_URL', function (rex_extension_point $ep) { 27 | if (!preg_match('@^rex:///(template|module|action)/(\d+)(?:/([^/]+))?@', $ep->getParam('file'), $match)) { 28 | return null; 29 | } 30 | 31 | $type = $match[1]; 32 | $id = $match[2]; 33 | 34 | if (!$this->getConfig($type.'s')) { 35 | return null; 36 | } 37 | 38 | if ('template' === $type) { 39 | $subtype = 'template'; 40 | } elseif (!isset($match[3])) { 41 | return null; 42 | } else { 43 | $subtype = $match[3]; 44 | } 45 | 46 | $path = rtrim(rex_developer_manager::getBasePath(), '/\\').'/'.$type.'s'; 47 | 48 | if (!$files = rex_developer_synchronizer::glob("$path/*/$id.rex_id", GLOB_NOSORT)) { 49 | return null; 50 | } 51 | 52 | $path = dirname($files[0]); 53 | 54 | if (!$files = rex_developer_synchronizer::glob("$path/*$subtype.php", GLOB_NOSORT)) { 55 | return null; 56 | } 57 | 58 | return rex_editor::factory()->getUrl($files[0], $ep->getParam('line')); 59 | }, rex_extension::LATE); 60 | -------------------------------------------------------------------------------- /lib/synchronizer_item.php: -------------------------------------------------------------------------------- 1 | content) 22 | */ 23 | public function __construct($id, $name, $updated, array $files = array()) 24 | { 25 | $this->id = $id; 26 | $this->name = $name; 27 | $this->updated = $updated; 28 | $this->files = $files; 29 | } 30 | 31 | /** 32 | * Sets the ID 33 | * 34 | * @param int $id 35 | */ 36 | public function setId($id) 37 | { 38 | $this->id = $id; 39 | } 40 | 41 | /** 42 | * Returns the ID 43 | * 44 | * @return int 45 | */ 46 | public function getId() 47 | { 48 | return $this->id; 49 | } 50 | 51 | /** 52 | * Sets the name 53 | * 54 | * @param string $name 55 | */ 56 | public function setName($name) 57 | { 58 | $this->name = $name; 59 | } 60 | 61 | /** 62 | * Returns the name 63 | * 64 | * @return string 65 | */ 66 | public function getName() 67 | { 68 | return $this->name; 69 | } 70 | 71 | /** 72 | * Sets the update timestamp 73 | * 74 | * @param int $updated 75 | */ 76 | public function setUpdated($updated) 77 | { 78 | $this->updated = $updated; 79 | } 80 | 81 | /** 82 | * Returns the update timestamp 83 | * 84 | * @return int 85 | */ 86 | public function getUpdated() 87 | { 88 | return $this->updated; 89 | } 90 | 91 | /** 92 | * Sets the item files and their content 93 | * 94 | * @param string[] $files Array of item files and their content (file=>content) 95 | */ 96 | public function setFiles(array $files) 97 | { 98 | $this->files = $files; 99 | } 100 | 101 | /** 102 | * Sets the content of an item file 103 | * 104 | * @param string $file File name 105 | * @param string|callable():string $content Content 106 | */ 107 | public function setFile($file, $content) 108 | { 109 | $this->files[$file] = $content; 110 | } 111 | 112 | /** 113 | * Returns all item files and their content 114 | * 115 | * @return string[] Array of all item files and their content (file=>content) 116 | */ 117 | public function getFiles() 118 | { 119 | return $this->files; 120 | } 121 | 122 | /** 123 | * Returns the content of the given item file 124 | * 125 | * @param string $file File name 126 | * @return string Content 127 | */ 128 | public function getFile($file) 129 | { 130 | if (!isset($this->files[$file])) { 131 | return ''; 132 | } 133 | if ((!is_string($file) || strlen($file) < 200) && is_callable($this->files[$file])) { 134 | $this->files[$file] = call_user_func($this->files[$file]); 135 | } 136 | return $this->files[$file]; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | REDAXO-AddOn: developer 2 | ======================= 3 | 4 | Das AddOn ermöglicht es, die Templates, Module, Actions, sowie die E-Mail-Templates von YForm über das Dateisystem (und somit mit beliebigem Editor) zu bearbeiten, bzw. neu anzulegen. 5 | 6 | ![Screenshot](https://raw.githubusercontent.com/FriendsOfREDAXO/developer/assets/developer.png) 7 | 8 | Mindestvoraussetzungen 9 | ---------------------- 10 | 11 | * PHP 5.5 12 | * REDAXO 5.2 13 | 14 | Installation 15 | ------------ 16 | 17 | 1. Über Installer laden oder Zip-Datei im AddOn-Ordner entpacken, der Ordner muss „developer“ heißen. 18 | 2. AddOn installieren und aktivieren. 19 | 3. Gegebenfalls die Einstellungen auf der Developer-Page anpassen. Standardmäßig sind alle drei Sychronisationen (Templates/Module/Actions) aktiviert. 20 | 21 | Benutzung 22 | --------- 23 | 24 | * Innerhalb des Ordners `redaxo/data/addons/developer` wird bei Bedarf jeweils ein Unterordner für Templates, Module und Actions angelegt. 25 | * Innerhalb der Unterordner wird für jedes einzelne Item (Template/Modul/Action) ein weiterer Unterordner angelegt. 26 | * Diese Ordner enthalten dann die Dateien, die synchronisiert werden. Neben der `metadata.yml`, welche unter anderem den Namen des Items enthält, sind dies die folgenden: 27 | - Templates: `template.php` 28 | - Module: `input.php`, `output.php` 29 | - Actions: `preview.php`, `presave.php`, `postsave.php` 30 | * Es wird nur synchronisiert, wenn man im Backend als Admin eingeloggt ist, dann aber auch, wenn man das Frontend aufruft. 31 | * Es können neue Items über das Dateisystem angelegt werden. Dazu genügt es einen neuen Ordner anzulegen mit mindestens einer der aufgelisteten Dateien. 32 | * Wenn die automatische Umbenennung deaktiviert ist, können die Dateien individuell umbenannt werden, sie müssen aber mit dem Standardnamen enden. Die `template.php` kann also zum Beispiel in `navigation.template.php` umbenannt werden. Developer wird die dann trotzdem finden und den Namen beibehalten. Optional kann ein Präfix bestehend aus ID und Name automatisch hinzugefügt werden. 33 | * Der Item-Ordner kann beliebig umbenannt werden. Als Zuordnung dient eine Datei `X.rex-id` innerhalb des Ordners, die nicht gelöscht werden darf. 34 | * Bei Umbennung über das Backend ändert Developer nichts an den Ordner- und Dateinamen, nur der Name innerhalb der `metadata.yml` wird aktualisiert. Über dieses Feld kann auch der Name im Backend über das Dateisystem geändert werden. 35 | * Nach dem Löschen eines Item-Ordners (oder einzelner Dateien) werden diese neu angelegt. Die Items müssen also regulär über das Backend gelöscht werden. 36 | * Nach dem Löschen eines Items über das Backend wird der Sychronisationsordner gelöscht, wenn die entsprechende Option nicht deaktivert ist. Ansonsten wird nur die `.rex-id` durch eine `.rex-ignore` ersetzt. 37 | 38 | 39 | Hinweise zur Synchronisation im Frontend 40 | ------------ 41 | * Damit die Synchronisation im Frontend funktioniert, muss hierzu die entsprechende Checkbox in den Einstellungen von developer aktiviert werden. 42 | * Damit die Synchronisation nach dem Speichern direkt im Frontend funktioniert, muss entweder der Debug-Modus aktiviert sein, oder die Seite im Frontend über die selbe Domain aufgerufen werden, mit welcher man sich im Backend eingeloggt hat, da ansonsten die Backend-Session nicht mit dem Frontend übereinstimmt (Beispiel: im Backend mit www. eingeloggt aber das Frontend ohne www. aufgerufen). Selbes gilt in Multidomain-Umgebungen und für http/https. 43 | 44 | 45 | Fehlerbehebung 46 | --------- 47 | Falls die Synchronisation von aktualisierten Dateien fehlschlägt, kann der Grund ein falscher Timestamp sein. Das `updatedate` in der Datenbank muss älter sein als der Zeitstempel der hochgeladenen Datei. 48 | 49 | 50 | Eigene Synchronisationen 51 | ------------------------ 52 | 53 | Über PlugIns oder andere AddOns ist es möglich, eigene Sychronisationen mit dem Dateisystem hinzuzufügen. Details dazu gibt es im [Wiki](https://github.com/friendsofredaxo/developer/wiki/Eigene-Synchronisationen). 54 | -------------------------------------------------------------------------------- /pages/settings.php: -------------------------------------------------------------------------------- 1 | setConfig(rex_post('config', [ 7 | ['templates', 'bool'], 8 | ['modules', 'bool'], 9 | ['actions', 'bool'], 10 | ['yform_email', 'bool'], 11 | ['sync_frontend', 'bool'], 12 | ['sync_backend', 'bool'], 13 | ['rename', 'bool'], 14 | ['dir_suffix', 'bool'], 15 | ['prefix', 'bool'], 16 | ['umlauts', 'bool'], 17 | ['delete', 'bool'], 18 | ])); 19 | 20 | echo rex_view::success($this->i18n('saved')); 21 | } 22 | 23 | $content = '
'; 24 | 25 | $formElements = []; 26 | 27 | if (rex_plugin::get('structure', 'content')->isAvailable()) { 28 | $n = []; 29 | $n['label'] = ''; 30 | $n['field'] = 'getConfig('templates') ? ' checked="checked"' : '') . ' />'; 31 | $formElements[] = $n; 32 | 33 | $n = []; 34 | $n['label'] = ''; 35 | $n['field'] = 'getConfig('modules') ? ' checked="checked"' : '') . ' />'; 36 | $formElements[] = $n; 37 | 38 | $n = []; 39 | $n['label'] = ''; 40 | $n['field'] = 'getConfig('actions') ? ' checked="checked"' : '') . ' />'; 41 | $formElements[] = $n; 42 | } 43 | 44 | if (rex_developer_manager::isYFormEmailAvailable()) { 45 | $n = []; 46 | $n['label'] = ''; 47 | $n['field'] = 'getConfig('yform_email') ? ' checked="checked"' : '') . ' />'; 48 | $formElements[] = $n; 49 | } 50 | 51 | $n = []; 52 | $n['label'] = ''; 53 | $n['field'] = 'getConfig('sync_frontend') ? ' checked="checked"' : '') . ' />'; 54 | $formElements[] = $n; 55 | 56 | $n = []; 57 | $n['label'] = ''; 58 | $n['field'] = 'getConfig('sync_backend') ? ' checked="checked"' : '') . ' />'; 59 | $formElements[] = $n; 60 | 61 | $n = []; 62 | $n['label'] = ''; 63 | $n['field'] = 'getConfig('rename') ? ' checked="checked"' : '') . ' />'; 64 | $formElements[] = $n; 65 | 66 | $n = []; 67 | $n['label'] = ''; 68 | $n['field'] = 'getConfig('dir_suffix') ? ' checked="checked"' : '') . ' />'; 69 | $formElements[] = $n; 70 | 71 | $n = []; 72 | $n['label'] = ''; 73 | $n['field'] = 'getConfig('prefix') ? ' checked="checked"' : '') . ' />'; 74 | $formElements[] = $n; 75 | 76 | $n = []; 77 | $n['label'] = ''; 78 | $n['field'] = 'getConfig('umlauts') ? ' checked="checked"' : '') . ' />'; 79 | $formElements[] = $n; 80 | 81 | $n = []; 82 | $n['label'] = ''; 83 | $n['field'] = 'getConfig('delete') ? ' checked="checked"' : '') . ' />'; 84 | $formElements[] = $n; 85 | 86 | $fragment = new rex_fragment(); 87 | $fragment->setVar('elements', $formElements, false); 88 | $content .= $fragment->parse('core/form/checkbox.php'); 89 | 90 | $formElements = []; 91 | 92 | $n = []; 93 | $n['field'] = ''; 94 | $formElements[] = $n; 95 | 96 | $fragment = new rex_fragment(); 97 | $fragment->setVar('flush', true); 98 | $fragment->setVar('elements', $formElements, false); 99 | $buttons = $fragment->parse('core/form/submit.php'); 100 | 101 | $fragment = new rex_fragment(); 102 | $fragment->setVar('class', 'edit'); 103 | $fragment->setVar('title', $this->i18n('settings')); 104 | $fragment->setVar('body', $content, false); 105 | $fragment->setVar('buttons', $buttons, false); 106 | $content = $fragment->parse('core/page/section.php'); 107 | 108 | echo ' 109 |
110 | ' . $content . ' 111 |
'; 112 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Version 3.9.3 – 02.06.2025 5 | -------------------------- 6 | 7 | * YForm 5 wird unterstützt bzgl. E-Mail-Templates-Synchronisation (@gharlan) 8 | * Navigations-Icon optimiert (@alxndr-w) 9 | 10 | Version 3.9.2 – 02.01.2023 11 | -------------------------- 12 | 13 | * Wenn der developer-Ordner gelöscht wurde, kam es zu einer Deprecated-Meldung (@gharlan) 14 | * Bei manchen Sonderzeichen im Namen konnte es unter Windows zu Fehlern kommen (@gharlan) 15 | * SQL-Escaping korrigiert (@staabm) 16 | * Rechtschreibfehler korrigiert (@eaCe) 17 | 18 | Version 3.9.1 – 01.10.2022 19 | -------------------------- 20 | 21 | * rexstan-Warnings gelöst (@staabm, @gharlan) 22 | 23 | Version 3.9.0 – 11.06.2020 24 | -------------------------- 25 | 26 | * YForm-E-Mail-Templates können synchronisiert werden (@jelleschutter) 27 | * Im Debug-Modus wird nun auch synchronisiert, wenn kein Backend-Admin eingeloggt ist (@gharlan) 28 | * Template/Module-Keys werden über die `metadata.yml` synchronisiert (@thorol, @gharlan) 29 | * Übersetzungen aktualisiert (@nandes2062, @interweave-media) 30 | * Es wird geprüft, ob das structure/content-Plugin verfügbar ist (@gharlan) 31 | * Readme erweitert (@Hirbod) 32 | * In Kombination mit adminer und dem Debug-Modus konnte es teils zu einem Fehler kommen (@gharlan) 33 | 34 | Version 3.8.0 – 01.01.2019 35 | -------------------------- 36 | 37 | * Editor-URL zu Modulen/Templates liefern (R5.7) 38 | * Readme als Subpage im Addon 39 | * Performance-Verbesserung 40 | * Nach Backup-Import wurden teils Module/Templates vom Stand vor dem Import wiederhergestellt 41 | 42 | Version 3.7.0 – 13.02.2018 43 | -------------------------- 44 | 45 | * Spanisch-Übersetzung 46 | * Neue Kommando-Optionen `--force-db` und `--force-files` 47 | * In Windows kam es teils zu exzessiven Vervielfältigungen der Module/Templates 48 | * Nach Backup-Import wurden teils Module/Templates vom Stand vor dem Import wiederhergestellt 49 | 50 | Version 3.6.1 – 26.10.2017 51 | -------------------------- 52 | 53 | * Die Ordnernamen enthielten HTML-Entities ("&" statt "&" etc.) 54 | 55 | Version 3.6.0 – 24.10.2017 56 | -------------------------- 57 | 58 | * Übersetzungen: Verbesserung Englisch, neu Portugiesisch und Schwedisch 59 | * Consolen-Command für das Synchronisieren über die cli 60 | * Hauptpfad kann über `rex_developer_manager::setBasePath()` geändert werden 61 | * Option zum Deaktivieren der Synchronisation im Backend 62 | * Option für ID-Suffix in Ordnernamen 63 | * Option für Erhaltung der Umlaute default inaktiv, Option ist nun deprecated 64 | * Bessere Ersetzung von Umlauten (insbesondere der Nicht-Deutschen) 65 | * Namen beginnend mit "translate:" werden auch im Ordnernamen übersetzt 66 | * Beim Aufruf von Medien über den Media Manager wird die Synchronisation nicht gestartet 67 | * Die mtime der Dateien wird nicht unnötig neu gesetzt (versursachte teilweise Reloadhinweise in manchen Editoren) 68 | 69 | Version 3.5.0 – 09.06.2016 70 | -------------------------- 71 | 72 | * Übertrag zu Friends Of REDAXO 73 | * Synchronisation in Frontend kann deaktiviert werden 74 | * Bugfix: "Module synchronisieren" konnte nicht deaktiviert werden 75 | 76 | Version 3.4.1 – 05.05.2016 77 | -------------------------- 78 | 79 | * Bugfix: Bei paralleler Entwicklung lokal/Server kam es teilweise zum ungewollten Löschen von Sync-Ordnern 80 | 81 | Version 3.4.0 – 18.01.2016 82 | -------------------------- 83 | 84 | * Anpassungen für REDAXO 5 final 85 | * Bugfix: Nach einem DB-Import wurden die Daten teilweise direkt wieder mit Altdaten überschrieben 86 | 87 | Version 3.3.1 – 10.05.2014 88 | -------------------------- 89 | 90 | * Bugfix: In Kombination mit Autoloadern konnte es zu Fehlermeldungen kommen 91 | 92 | Version 3.3.0 – 14.02.2013 93 | -------------------------- 94 | 95 | * Neuer EP: DEVELOPER_MANAGER_START 96 | * Performanceverbesserung 97 | * Bugfix: Unter bestimmten Umständen wurden die Ordner (teilweise auch DB-Einträge) bei jedem Aufruf vervielfältigt 98 | 99 | Version 3.2.0 – 08.11.2013 100 | -------------------------- 101 | 102 | * Min. REDAXO-Version: 4.3.2 103 | * Korrekte Dateinamen (mit Präfix-Option fehlte ein Punkt vor input.php etc.) 104 | * Optional können Umlaute wieder ersetzt werden 105 | * Die Einstellungen wirken sich beim Speichern direkt aus 106 | * Optional können die Ordner- und Dateinamen automatisch aktuell gehalten werden 107 | * Optional können die Item-Ordner automatisch gelöscht werden nach dem Löschen über das Backend 108 | 109 | Version 3.1.1 – 14.08.2013 110 | -------------------------- 111 | 112 | * Die Einstellungen werden außerhalb des Addonordners in /redaxo/include/data/addons/developer gespeichert 113 | * Neuer Standardpfad für die synchronisierten Dateien: /redaxo/include/data/addons/developer 114 | 115 | Version 3.1.0 - 03.08.2013 116 | -------------------------- 117 | 118 | * Parallele Entwicklung für REDAXO 5 119 | * Optional kann allen Dateien ein Präfix bestehend aus ID und Name vorangestellt werden 120 | * Die ID wird im Namen der ID-Datei gespeichert statt im Inhalt ("1.rex_id" etc.) 121 | * PlugIn-Unterstützung 122 | * Umlaute/Sonderzeichen im Namen werden beibehalten, nur wirklich problematische werden ersetzt 123 | 124 | Version 3.0.0 – 20.02.2013 125 | -------------------------- 126 | 127 | * Grunderneuerung (Mindestvoraussetzungen: PHP 5.3.3, REDAXO 4.3) 128 | * Pro Template/Module/Action ein Ordner (dadurch Verwaltung mit git möglich) 129 | * Templates/Module/Actions können über das Dateisystem neu angelegt werden 130 | * Metadaten werden jeweils über eine YAML-Datei verwaltet 131 | * Weitere Synchronisationen können über PlugIns bzw. andere AddOns hinzugefügt werden 132 | 133 | Version 2.2.2 – 10.10.2012 134 | -------------------------- 135 | 136 | * Alle Synchronisierungen standardmäßig aktiviert 137 | * Bugfix: Nach DB-Import wurden Templates/Module direkt wieder mit den vorherigen überschrieben 138 | 139 | Version 2.2.1 – 22.03.2011 140 | -------------------------- 141 | 142 | * Behebung kleinerer Bugs 143 | * Neues Dateinamenschema mit Template/Module-Namen am Anfang für alphabetische Sortierung 144 | 145 | Version 2.2.0 – 05.12.2010 146 | -------------------------- 147 | 148 | * Synchronisation der Actions 149 | * Verbesserte Aufräumarbeiten 150 | * Synchronisation erst nach ADDONS_INCLUDED 151 | 152 | Version 2.1.0 – 19.11.2010 153 | -------------------------- 154 | 155 | * neue Codebasis 156 | -------------------------------------------------------------------------------- /lib/manager.php: -------------------------------------------------------------------------------- 1 | array(), 15 | self::START_LATE => array() 16 | ); 17 | 18 | private static $basePath; 19 | 20 | public static function setBasePath($basePath) 21 | { 22 | self::$basePath = $basePath; 23 | } 24 | 25 | public static function getBasePath() 26 | { 27 | return self::$basePath ?: rex_path::addonData('developer'); 28 | } 29 | 30 | /** 31 | * Registers a new synchronizer 32 | * 33 | * @param rex_developer_synchronizer $synchronizer The synchronizer object 34 | * @param int $start Flag, whether the synchronizer should start at the end of the request 35 | */ 36 | public static function register(rex_developer_synchronizer $synchronizer, $start = self::START_EARLY) 37 | { 38 | self::$synchronizers[$start][] = $synchronizer; 39 | } 40 | 41 | /** 42 | * Registers the default synchronizers for templates, modules and actions 43 | */ 44 | private static function registerDefault() 45 | { 46 | $page = rex_be_controller::getCurrentPage(); 47 | // workaround for https://github.com/redaxo/redaxo/issues/2900 48 | $function = rex_request('function', '', null); 49 | $function = is_string($function) ? $function : null; 50 | $save = rex_request('save', 'string', ''); 51 | $addon = rex_addon::get('developer'); 52 | 53 | $structureContent = rex_plugin::get('structure', 'content'); 54 | 55 | if ($structureContent->isAvailable() && $addon->getConfig('templates')) { 56 | $metadata = array(); 57 | if (rex_string::versionCompare($structureContent->getVersion(), '2.9', '>=')) { 58 | $metadata = array('key' => 'string'); 59 | } 60 | $metadata = array_merge($metadata, array('active' => 'boolean', 'attributes' => 'json')); 61 | 62 | $synchronizer = new rex_developer_synchronizer_default( 63 | 'templates', 64 | rex::getTable('template'), 65 | array('content' => 'template.php'), 66 | $metadata 67 | ); 68 | $callback = function (rex_developer_synchronizer_item $item) { 69 | $template = new rex_template($item->getId()); 70 | $template->deleteCache(); 71 | }; 72 | $synchronizer->setEditedCallback($callback); 73 | $synchronizer->setDeletedCallback($callback); 74 | self::register( 75 | $synchronizer, 76 | $page == 'templates' && ((($function == 'add' || $function == 'edit') && $save == 'ja') || $function == 'delete') ? self::START_LATE : self::START_EARLY 77 | ); 78 | } 79 | 80 | if ($structureContent->isAvailable() && $addon->getConfig('modules')) { 81 | $metadata = array(); 82 | if (rex_string::versionCompare($structureContent->getVersion(), '2.10', '>=')) { 83 | $metadata = array('key' => 'string'); 84 | } 85 | 86 | $synchronizer = new rex_developer_synchronizer_default( 87 | 'modules', 88 | rex::getTable('module'), 89 | array('input' => 'input.php', 'output' => 'output.php'), 90 | $metadata 91 | ); 92 | $callback = function (rex_developer_synchronizer_item $item) { 93 | $sql = rex_sql::factory(); 94 | $sql->setQuery(' 95 | SELECT DISTINCT(article.id) 96 | FROM '.rex::getTable('article').' article 97 | LEFT JOIN '.rex::getTable('article_slice').' slice 98 | ON article.id = slice.article_id 99 | WHERE slice.module_id='.$item->getId() 100 | ); 101 | for ($i = 0, $rows = $sql->getRows(); $i < $rows; ++$i) { 102 | rex_article_cache::delete($sql->getValue('article.id')); 103 | $sql->next(); 104 | } 105 | }; 106 | $synchronizer->setEditedCallback($callback); 107 | $synchronizer->setDeletedCallback($callback); 108 | self::register( 109 | $synchronizer, 110 | $page == 'modules/modules' && ((($function == 'add' || $function == 'edit') && $save == '1') || $function == 'delete') ? self::START_LATE : self::START_EARLY 111 | ); 112 | } 113 | 114 | if ($structureContent->isAvailable() && $addon->getConfig('actions')) { 115 | $synchronizer = new rex_developer_synchronizer_default( 116 | 'actions', 117 | rex::getTable('action'), 118 | array('preview' => 'preview.php', 'presave' => 'presave.php', 'postsave' => 'postsave.php'), 119 | array('previewmode' => 'int', 'presavemode' => 'int', 'postsavemode' => 'int') 120 | ); 121 | self::register( 122 | $synchronizer, 123 | $page == 'modules/actions' && ((($function == 'add' || $function == 'edit') && $save == '1') || $function == 'delete') ? self::START_LATE : self::START_EARLY 124 | ); 125 | } 126 | 127 | if (self::isYFormEmailAvailable() && $addon->getConfig('yform_email')) { 128 | $synchronizer = new rex_developer_synchronizer_default( 129 | 'yform_email', 130 | rex::getTable('yform_email_template'), 131 | array('body' => 'body.php', 'body_html' => 'body_html.php'), 132 | array('mail_from' => 'string', 'mail_from_name' => 'string', 'mail_reply_to' => 'string', 'mail_reply_to_name' => 'string', 'subject' => 'string', 'attachments' => 'string') 133 | ); 134 | $synchronizer->setCommonCreateUpdateColumns(false); 135 | self::register( 136 | $synchronizer, 137 | $page == 'yform/email/index' && ($function == 'add' || $function == 'edit' || $function == 'delete') ? self::START_LATE : self::START_EARLY 138 | ); 139 | } 140 | } 141 | 142 | /** 143 | * Starts the main developer process 144 | * 145 | * The method registers the default synchronizers and the extensions which will start the synchronizer objects 146 | * 147 | * @param bool|int $force Flag, whether the synchronizers should run in force mode (`rex_developer_synchronizer::FORCE_DB/FILES`) 148 | */ 149 | public static function start($force = false) 150 | { 151 | rex_extension::registerPoint(new rex_extension_point('DEVELOPER_MANAGER_START', '', [], true)); 152 | 153 | self::registerDefault(); 154 | 155 | if (method_exists('rex', 'getConsole') && rex::getConsole()) { 156 | self::synchronize(null, $force); 157 | } elseif (rex_be_controller::getCurrentPagePart(1) === 'backup') { 158 | rex_extension::register('BACKUP_AFTER_DB_IMPORT', function () { 159 | rex_developer_manager::synchronize(null, rex_developer_synchronizer::FORCE_DB); 160 | }); 161 | } elseif (rex_be_controller::getCurrentPagePart(1) === 'developer' && rex_get('function', 'string') === 'update') { 162 | rex_extension::register('RESPONSE_SHUTDOWN', function () { 163 | rex_developer_manager::synchronize(null, rex_developer_synchronizer::FORCE_DB); 164 | }); 165 | } else { 166 | self::synchronize(self::START_EARLY, $force); 167 | rex_extension::register('RESPONSE_SHUTDOWN', function () use ($force) { 168 | rex_developer_manager::synchronize(self::START_LATE, $force); 169 | }); 170 | } 171 | } 172 | 173 | /** 174 | * Runs the synchronizer objects 175 | * 176 | * @param int|null $type Flag, which synchronizers should start. If the value is null, all synchronizers will start 177 | * @param bool|int $force Flag, whether the synchronizers should run in force mode (`rex_developer_synchronizer::FORCE_DB/FILES`) 178 | * @see rex_developer_synchronizer::run 179 | */ 180 | public static function synchronize($type = null, $force = false) 181 | { 182 | $run = function (rex_developer_synchronizer $synchronizer) use ($force) { 183 | $synchronizer->run($force); 184 | }; 185 | if ($type === null) { 186 | foreach (self::$synchronizers as $synchronizers) { 187 | array_walk($synchronizers, $run); 188 | } 189 | } else { 190 | array_walk(self::$synchronizers[$type], $run); 191 | } 192 | } 193 | 194 | public static function isYFormEmailAvailable(): bool 195 | { 196 | $yform = rex_addon::get('yform'); 197 | if ($yform->isAvailable() && rex_string::versionCompare($yform->getVersion(), '5.0-dev', '>=')) { 198 | return true; 199 | } 200 | 201 | $yformEmail = $yform->getPlugin('email'); 202 | 203 | return $yformEmail->isAvailable() && rex_string::versionCompare($yformEmail->getVersion(), '3.4b1', '>='); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /lib/synchronizer_default.php: -------------------------------------------------------------------------------- 1 | file which contains the columns that should be synchronized to single files 32 | * @param string[] $metadata An associative array column=>type which contains the columns (and their content type) that should be synchronized together to the metadata file 33 | */ 34 | public function __construct($dirname, $table, array $files, array $metadata = array()) 35 | { 36 | $this->table = $table; 37 | $this->columns = array_flip($files); 38 | $this->metadata = array_merge(array('name' => 'string'), $metadata); 39 | $files[] = self::METADATA_FILE; 40 | parent::__construct($dirname, $files); 41 | } 42 | 43 | /** 44 | * Sets a callback function which will be called if a new item is added by the file system 45 | * 46 | * @param callable $callback 47 | */ 48 | public function setAddedCallback($callback) 49 | { 50 | $this->addedCallback = $callback; 51 | } 52 | 53 | /** 54 | * Sets a callback function which will be called if an existing item is edited by the file system 55 | * 56 | * @param callable $callback 57 | */ 58 | public function setEditedCallback($callback) 59 | { 60 | $this->editedCallback = $callback; 61 | } 62 | 63 | /** 64 | * Sets a callback function which will be called if an existing item is deleted by the file system 65 | * 66 | * @param callable $callback 67 | */ 68 | public function setDeletedCallback($callback) 69 | { 70 | $this->deletedCallback = $callback; 71 | } 72 | 73 | /** 74 | * Sets the name of the ID column 75 | * 76 | * @param string $idColumn Column name 77 | */ 78 | public function setIdColumn($idColumn) 79 | { 80 | $this->idColumn = $idColumn; 81 | } 82 | 83 | /** 84 | * Sets the name of the column which contains the name of the item 85 | * 86 | * @param string $nameColumn Column name 87 | */ 88 | public function setNameColumn($nameColumn) 89 | { 90 | $this->nameColumn = $nameColumn; 91 | } 92 | 93 | /** 94 | * Sets the name of the column which contains the updated timestamp 95 | * 96 | * @param string $updatedColumn Column name 97 | */ 98 | public function setUpdatedColumn($updatedColumn) 99 | { 100 | $this->updatedColumn = $updatedColumn; 101 | } 102 | 103 | /** 104 | * Sets whether the table has the common create and update columns (createdate, createuser, updatedate, updateuser) 105 | * 106 | * @param bool $commonCreateUpdateColumns 107 | */ 108 | public function setCommonCreateUpdateColumns($commonCreateUpdateColumns) 109 | { 110 | if ($commonCreateUpdateColumns) { 111 | $this->commonCreateUpdateColumns = true; 112 | $this->updatedColumn = 'updatedate'; 113 | } else { 114 | $this->commonCreateUpdateColumns = false; 115 | } 116 | } 117 | 118 | /** 119 | * {@inheritDoc} 120 | */ 121 | protected function getItems() 122 | { 123 | $defaultLang = rex::getProperty('lang'); 124 | $lang = rex_i18n::getLocale(); 125 | if ($defaultLang !== $lang) { 126 | rex_i18n::setLocale($defaultLang, false); 127 | } 128 | 129 | try { 130 | $items = array(); 131 | $sql = rex_sql::factory(); 132 | $sql->setQuery('SELECT * FROM ' . $sql->escapeIdentifier($this->table)); 133 | for ($i = 0, $rows = $sql->getRows(); $i < $rows; ++$i, $sql->next()) { 134 | $name = rex_i18n::translate($sql->getValue($this->nameColumn), false); 135 | $item = new rex_developer_synchronizer_item($sql->getValue($this->idColumn), $name, $sql->getDateTimeValue($this->updatedColumn)); 136 | foreach ($this->columns as $file => $column) { 137 | $item->setFile($file, $sql->getValue($column)); 138 | } 139 | $metadata = array(); 140 | foreach ($this->metadata as $column => $type) { 141 | $metadata[$column] = self::cast($sql->getValue($column), $type); 142 | } 143 | $item->setFile(self::METADATA_FILE, function() use ($metadata) { 144 | return rex_string::yamlEncode($metadata); 145 | }); 146 | $items[] = $item; 147 | } 148 | } finally { 149 | if ($defaultLang !== $lang) { 150 | rex_i18n::setLocale($lang, false); 151 | } 152 | } 153 | 154 | return $items; 155 | } 156 | 157 | /** 158 | * {@inheritDoc} 159 | */ 160 | protected function addItem(rex_developer_synchronizer_item $item) 161 | { 162 | $sql = rex_sql::factory(); 163 | $id = $item->getId(); 164 | if ($id) { 165 | $sql->setQuery('SELECT ' . $sql->escapeIdentifier($this->idColumn) . ' FROM ' . $sql->escapeIdentifier($this->table) . ' WHERE ' . $sql->escapeIdentifier($this->idColumn) . ' = ' . $id); 166 | if ($sql->getRows() == 0) { 167 | $sql->setValue($this->idColumn, $id); 168 | } 169 | } 170 | $sql->setTable($this->table); 171 | $sql->setValue($this->nameColumn, $item->getName()); 172 | if ($this->commonCreateUpdateColumns) { 173 | $user = rex::getUser() ? rex::getUser()->getLogin() : 'console'; 174 | $sql->setDateTimeValue('updatedate', $item->getUpdated()); 175 | $sql->setValue('updateuser', $user); 176 | $sql->setDateTimeValue('createdate', $item->getUpdated()); 177 | $sql->setValue('createuser', $user); 178 | } else { 179 | $sql->setDateTimeValue($this->updatedColumn, $item->getUpdated()); 180 | } 181 | $files = $item->getFiles(); 182 | if (isset($files[self::METADATA_FILE])) { 183 | $metadata = rex_string::yamlDecode($files[self::METADATA_FILE]); 184 | foreach ($this->metadata as $column => $type) { 185 | if (array_key_exists($column, $metadata)) { 186 | $sql->setValue($column, self::toString($metadata[$column], $type)); 187 | } 188 | } 189 | unset($files[self::METADATA_FILE]); 190 | } 191 | foreach ($files as $file => $content) { 192 | $sql->setValue($this->columns[$file], $content); 193 | } 194 | $sql->insert(); 195 | 196 | $id = (int) $sql->getLastId(); 197 | $item->setId($id); 198 | if ($this->addedCallback) { 199 | call_user_func($this->addedCallback, $item); 200 | } 201 | return $id; 202 | } 203 | 204 | /** 205 | * {@inheritDoc} 206 | */ 207 | protected function editItem(rex_developer_synchronizer_item $item) 208 | { 209 | $sql = rex_sql::factory(); 210 | $sql->setTable($this->table); 211 | $sql->setWhere([$this->idColumn => $item->getId()]); 212 | if ($this->commonCreateUpdateColumns) { 213 | $sql->setDateTimeValue('updatedate', $item->getUpdated()); 214 | $sql->setValue('updateuser', rex::getUser() ? rex::getUser()->getLogin() : 'console'); 215 | } else { 216 | $sql->setDateTimeValue($this->updatedColumn, $item->getUpdated()); 217 | } 218 | $files = $item->getFiles(); 219 | if (isset($files[self::METADATA_FILE])) { 220 | $metadata = rex_string::yamlDecode($files[self::METADATA_FILE]); 221 | foreach ($this->metadata as $column => $type) { 222 | if (array_key_exists($column, $metadata)) { 223 | $sql->setValue($column, self::toString($metadata[$column], $type)); 224 | } 225 | } 226 | unset($files[self::METADATA_FILE]); 227 | } 228 | foreach ($files as $file => $content) { 229 | $sql->setValue($this->columns[$file], $content); 230 | } 231 | $sql->update(); 232 | if ($this->editedCallback) { 233 | call_user_func($this->editedCallback, $item); 234 | } 235 | } 236 | 237 | /** 238 | * {@inheritDoc} 239 | */ 240 | protected function deleteItem(rex_developer_synchronizer_item $item) 241 | { 242 | $sql = rex_sql::factory(); 243 | $sql->setTable($this->table); 244 | $sql->setWhere([$this->idColumn => $item->getId()]); 245 | $sql->delete(); 246 | 247 | if ($this->deletedCallback) { 248 | call_user_func($this->deletedCallback, $item); 249 | } 250 | } 251 | 252 | /** 253 | * Casts a value by the given type 254 | * 255 | * @param string $value Value 256 | * @param string $type Type 257 | * @return mixed 258 | */ 259 | private static function cast($value, $type) 260 | { 261 | if (null === $value) { 262 | return null; 263 | } 264 | 265 | switch ($type) { 266 | case 'bool': 267 | case 'boolean': 268 | return (boolean) $value; 269 | case 'int': 270 | case 'integer': 271 | return (integer) $value; 272 | case 'serialize': 273 | return unserialize($value); 274 | case 'json': 275 | return json_decode($value, true); 276 | case 'string': 277 | default: 278 | return (string) $value; 279 | } 280 | } 281 | 282 | /** 283 | * Converts a value from the given type to a string 284 | * 285 | * @param mixed $value Value 286 | * @param string $type Type 287 | * @return null|string 288 | */ 289 | private static function toString($value, $type) 290 | { 291 | if (null === $value) { 292 | return null; 293 | } 294 | 295 | switch ($type) { 296 | case 'serialize': 297 | return serialize($value); 298 | case 'json': 299 | return json_encode($value); 300 | default: 301 | return (string) $value; 302 | } 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /lib/synchronizer.php: -------------------------------------------------------------------------------- 1 | dirname = $dirname; 32 | $this->baseDir = rex_developer_manager::getBasePath() . '/' . $dirname . '/'; 33 | 34 | $this->files = $files; 35 | } 36 | 37 | /** 38 | * The method should return all items from the base system which should be synchronized to the file system 39 | * 40 | * @return rex_developer_synchronizer_item[] 41 | */ 42 | abstract protected function getItems(); 43 | 44 | /** 45 | * The method is called, when a new item is created by the file system 46 | * 47 | * Use the method to add the new item to the base system. The method should return the new ID of the item. 48 | * 49 | * @param rex_developer_synchronizer_item $item New item 50 | * @return int ID of new item 51 | */ 52 | abstract protected function addItem(rex_developer_synchronizer_item $item); 53 | 54 | /** 55 | * The method is called, when an existing item is edited by the file system 56 | * 57 | * Use the method to edit the item in the base system 58 | * 59 | * @param rex_developer_synchronizer_item $item 60 | */ 61 | abstract protected function editItem(rex_developer_synchronizer_item $item); 62 | 63 | /** 64 | * The method is called, when an existing item is deleted by the file system (`FORCE_FILES` is activated) 65 | * 66 | * Use the method to delete the item in the base system 67 | * 68 | * @param rex_developer_synchronizer_item $item 69 | */ 70 | protected function deleteItem(rex_developer_synchronizer_item $item) 71 | { 72 | } 73 | 74 | /** 75 | * Runs the synchronizer 76 | * 77 | * @param bool $force Flag, whether the synchronizers should run in force mode (`rex_developer_synchronizer::FORCE_DB/FILES`) 78 | */ 79 | public function run($force = false) 80 | { 81 | $idLists = rex_config::get('developer', 'items', array()); 82 | $idList = isset($idLists[$this->dirname]) ? $idLists[$this->dirname] : array(); 83 | 84 | if (isset($idList[0])) { 85 | $idList = array_flip($idList); 86 | } 87 | 88 | $origIdList = $idList; 89 | 90 | list($existing, $new) = $this->getNewAndExistingDirs(); 91 | $this->synchronizeReceivedItems($idList, $existing, $force); 92 | $this->removeItems($idList, $existing, $force); 93 | $this->addNewItems($idList, $existing, true); 94 | $this->addNewItems($idList, $new, false); 95 | 96 | if ($idList !== $origIdList) { 97 | $idLists[$this->dirname] = $idList; 98 | rex_config::set('developer', 'items', $idLists); 99 | } 100 | } 101 | 102 | private function getNewAndExistingDirs() 103 | { 104 | $existing = array(); 105 | $new = array(); 106 | $dirs = self::glob($this->baseDir . '*', GLOB_ONLYDIR | GLOB_NOSORT | GLOB_MARK); 107 | if (is_array($dirs)) { 108 | foreach ($dirs as $dir) { 109 | if (!file_exists($dir . self::IGNORE_FILE)) { 110 | $file = basename(self::getFile($dir, self::ID_FILE)); 111 | if ( 112 | file_exists($dir . $file) && 113 | (sscanf($file, '%d' . self::ID_FILE, $id) || ($id = ((int) rex_file::get($dir . $file)))) 114 | ) { 115 | if (isset($existing[$id])) { 116 | trigger_error( 117 | 'There are two item directories with the same ID: "' . $existing[$id] . '" and "' . basename($dir) . '"', 118 | E_USER_ERROR 119 | ); 120 | } 121 | $existing[$id] = basename($dir); 122 | } else { 123 | $new[] = basename($dir); 124 | } 125 | } 126 | } 127 | } 128 | return array($existing, $new); 129 | } 130 | 131 | private function synchronizeReceivedItems(&$idList, &$existing, $force = false) 132 | { 133 | $force = $force ? (int) $force : false; 134 | 135 | foreach ($this->getItems() as $item) { 136 | $id = $item->getId(); 137 | $name = $item->getName(); 138 | 139 | $existingDir = null; 140 | if (isset($existing[$id])) { 141 | $existingDir = $existing[$id]; 142 | unset($existing[$id]); 143 | } elseif (self::FORCE_FILES === $force) { 144 | $this->deleteItem($item); 145 | unset($idList[$id]); 146 | 147 | continue; 148 | } 149 | 150 | if (rex_config::get('developer', 'rename') || !$existingDir) { 151 | $dirBase = self::getFilename($name); 152 | 153 | if (rex_config::get('developer', 'dir_suffix')) { 154 | $dirBase .= ' ['.$id.']'; 155 | } 156 | 157 | $dir = $dirBase; 158 | $i = 1; 159 | while ((!$existingDir || !self::equalFilenames($existingDir, $dir)) && file_exists($this->baseDir . $dir)) { 160 | $dir = $dirBase . ' [' . ++$i . ']'; 161 | } 162 | if (!$existingDir) { 163 | if (!rex_file::put($this->baseDir . $dir . '/' . $id . self::ID_FILE, '')) { 164 | continue; 165 | } 166 | } elseif (!self::equalFilenames($existingDir, $dir)) { 167 | rename($this->baseDir . $existingDir, $this->baseDir . $dir); 168 | } 169 | $dir = $this->baseDir . $dir . '/'; 170 | } else { 171 | $dir = $this->baseDir . $existingDir . '/'; 172 | } 173 | 174 | $lastUpdated = self::FORCE_DB !== $force && isset($idList[$id]) ? $idList[$id] : 0; 175 | $updated = self::FORCE_FILES === $force ? 0 : max(1, $item->getUpdated()); 176 | $dbUpdated = $updated; 177 | $updateFiles = array(); 178 | $files = array(); 179 | $prefix = ''; 180 | if (rex_config::get('developer', 'prefix')) { 181 | $prefix = $id . '.' . $name . '.'; 182 | } 183 | foreach ($this->files as $file) { 184 | $filePath = self::getFile($dir, $file, $prefix, rex_config::get('developer', 'rename')); 185 | $files[] = $filePath; 186 | 187 | $fileMtime = @filemtime($filePath); 188 | $fileExists = $fileMtime !== false; 189 | $fileUpdated = self::FORCE_DB !== $force && $fileExists ? $fileMtime : 0; 190 | 191 | if ($dbUpdated > $fileUpdated && $dbUpdated > $lastUpdated || !$fileExists) { 192 | rex_file::put($filePath, $item->getFile($file)); 193 | touch($filePath, $updated); 194 | } elseif ($fileUpdated > $dbUpdated) { 195 | $updated = max($updated, $fileUpdated); 196 | $updateFiles[$file] = rex_file::get($filePath); 197 | } 198 | } 199 | if ($dbUpdated != $updated) { 200 | $this->editItem(new rex_developer_synchronizer_item($id, $name, $updated, $updateFiles)); 201 | } 202 | 203 | $idList[$id] = $updated; 204 | } 205 | } 206 | 207 | private function removeItems(&$idList, &$existing, $force = false) 208 | { 209 | if (self::FORCE_FILES === $force) { 210 | return; 211 | } 212 | 213 | foreach ($existing as $id => $dir) { 214 | $dir = $this->baseDir . $dir . '/'; 215 | if (self::FORCE_DB === $force || isset($idList[$id])) { 216 | unset($existing[$id]); 217 | unset($idList[$id]); 218 | if (rex_config::get('developer', 'delete')) { 219 | rex_dir::delete($dir); 220 | } else { 221 | rex_file::put($dir . self::IGNORE_FILE, ''); 222 | unlink(self::getFile($dir, self::ID_FILE)); 223 | } 224 | } 225 | } 226 | } 227 | 228 | private function addNewItems(&$idList, $dirs, $withId) 229 | { 230 | foreach ($dirs as $i => $dir) { 231 | $dir = $this->baseDir . $dir . '/'; 232 | $addFiles = array(); 233 | $add = false; 234 | $updated = time(); 235 | foreach ($this->files as $file) { 236 | $filePath = self::getFile($dir, $file); 237 | if (file_exists($filePath)) { 238 | $add = true; 239 | $addFiles[$file] = rex_file::get($filePath); 240 | touch($filePath, $updated); 241 | } else { 242 | $addFiles[$file] = ''; 243 | } 244 | } 245 | $id = $withId ? $i : null; 246 | $name = strtr(basename($dir), '_', ' '); 247 | if ($add && $id = $this->addItem(new rex_developer_synchronizer_item($id, $name, $updated, $addFiles))) { 248 | rex_file::put($dir . $id . self::ID_FILE, ''); 249 | $idList[$id] = $updated; 250 | } 251 | } 252 | } 253 | 254 | /** 255 | * Gets the real path for an item file 256 | * 257 | * Item files can be prefixed by the user, so e.g. "example.template.php" will match the item file "template.php" 258 | * 259 | * @param string $dir Directory 260 | * @param string $file File name 261 | * @param string $defaultPrefix Default prefix 262 | * @param bool $rename 263 | * @return string Real File path 264 | */ 265 | protected static function getFile($dir, $file, $defaultPrefix = '', $rename = false) 266 | { 267 | $defaultPath = $dir . self::getFilename($defaultPrefix . $file); 268 | if (file_exists($defaultPath)) { 269 | $path = $defaultPath; 270 | } elseif (file_exists($dir . $file)) { 271 | $path = $dir . $file; 272 | } elseif (is_array($glob = self::glob($dir . '*' . $file)) && !empty($glob)) { 273 | $path = $dir . basename($glob[0]); 274 | } 275 | if (isset($path)) { 276 | if ($rename && !self::equalFilenames($path, $defaultPath)) { 277 | rename($path, $defaultPath); 278 | return $defaultPath; 279 | } 280 | return $path; 281 | } 282 | return $defaultPath; 283 | } 284 | 285 | /** 286 | * Replaces special chars 287 | * 288 | * @param string $name 289 | * @return string 290 | */ 291 | protected static function getFilename($name) 292 | { 293 | if (!rex_config::get('developer', 'umlauts')) { 294 | $name = str_replace( 295 | array('Ä', 'Ö', 'Ü', 'ä', 'ö', 'ü', 'ß'), 296 | array('Ae', 'Oe', 'Ue', 'ae', 'oe', 'ue', 'ss'), 297 | $name 298 | ); 299 | $name = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $name); 300 | } 301 | 302 | $name = preg_replace('@[\\\\|:<>?*"\'+]@', '', $name); 303 | $name = strtr($name, '[]/', '()-'); 304 | 305 | return ltrim(rtrim($name, ' .')); 306 | } 307 | 308 | /** 309 | * Checks whether the filenames are equal (independent of UTF8 NFC and NFD) 310 | * 311 | * @param string $filename1 312 | * @param string $filename2 313 | * @return bool 314 | */ 315 | protected static function equalFilenames($filename1, $filename2) 316 | { 317 | $search = array("A\xcc\x88", "O\xcc\x88", "U\xcc\x88", "a\xcc\x88", "o\xcc\x88", "u\xcc\x88"); 318 | $replace = array('Ä', 'Ö', 'Ü', 'ä', 'ö', 'ü'); 319 | $filename1 = str_replace($search, $replace, $filename1); 320 | $filename2 = str_replace($search, $replace, $filename2); 321 | 322 | return $filename1 === $filename2; 323 | } 324 | 325 | public static function glob($pattern, $flags = 0) 326 | { 327 | $pattern = str_replace(['[',']',"\f[","\f]"], ["\f[","\f]",'[[]','[]]'], $pattern); 328 | return glob($pattern, $flags); 329 | } 330 | } 331 | --------------------------------------------------------------------------------