diff --git a/DEVTOOLS.md b/DEVTOOLS.md new file mode 100644 index 0000000000..dfc3231b84 --- /dev/null +++ b/DEVTOOLS.md @@ -0,0 +1,42 @@ +# Additional development tools + +Various components used within Textpattern (such as the bundled themes and language translations) are maintained in other repositories. Textpattern has a simple development toolset built on [Node.js](https://nodejs.org/) to pull the distribution files of those repositories into the core as required. + +You can install Node.js using the [installer](https://nodejs.org/en/download/) or [package manager](https://nodejs.org/en/download/package-manager/). + +Install required dev tools: + +```ShellSession +npm install +``` + +Pull the following components from the CLI: + +```ShellSession +npm run get-default-theme +npm run get-classic-admin-theme +npm run get-hive-admin-theme +npm run get-pophelp +npm run get-textpacks +npm run get-dependencies +``` + +To request a specific tag or branch: + +```ShellSession +npm run get-default-theme 4.9.0 +npm run get-classic-admin-theme 4.9.0 +npm run get-classic-admin-theme 4.9.x +npm run get-hive-admin-theme 4.9.x +npm run get-textpacks 4.9.x +``` + +Release tools: + +Usage: `npm run txp-gitdist [dest-dir]` (`dest-dir` defaults to a temporary location). + +```ShellSession +npm run txp-index +npm run txp-checksums ./textpattern +npm run txp-gitdist 1.2.3 ../my-dest-dir +``` diff --git a/README.md b/README.md index 7927642539..6e123d4094 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,15 @@ Ensure the server meets or exceeds the [system requirements](https://textpattern ## Download Textpattern -The current production release is version 4.8.8. It can be downloaded from the Textpattern website or GitHub in .zip and .tar.gz varieties. +The current production release is version 4.9.0. It can be downloaded from the Textpattern website or GitHub in .zip, .tar.gz, and .tar.xz varieties. -If you want to use the multi-site functionality in Textpattern, get the .tar.gz archive. +If you want to use the multi-site functionality in Textpattern, get the .tar.gz or .tar.xz archive. | | textpattern.com | GitHub | |--------|:-------:|:-----:| -| .zip | [Download](https://textpattern.com/file_download/118/textpattern-4.8.8.zip) | [Download](https://github.com/textpattern/textpattern/releases/download/4.8.8/textpattern-4.8.8.zip) | -| .tar.gz | [Download](https://textpattern.com/file_download/117/textpattern-4.8.8.tar.gz) | [Download](https://github.com/textpattern/textpattern/releases/download/4.8.8/textpattern-4.8.8.tar.gz) | +| .zip | [Download](https://textpattern.com/file_download/124/textpattern-4.9.0.zip) | [Download](https://github.com/textpattern/textpattern/releases/download/4.9.0/textpattern-4.9.0.zip) | +| .tar.gz | [Download](https://textpattern.com/file_download/125/textpattern-4.9.0.tar.gz) | [Download](https://github.com/textpattern/textpattern/releases/download/4.9.0/textpattern-4.9.0.tar.gz) | +| .tar.xz | [Download](https://textpattern.com/file_download/126/textpattern-4.9.0.tar.xz) | [Download](https://github.com/textpattern/textpattern/releases/download/4.9.0/textpattern-4.9.0.tar.xz) | ## Install Textpattern @@ -44,11 +45,11 @@ Please see [README.txt](https://github.com/textpattern/textpattern/blob/main/REA ## Upgrade Textpattern -Please see [README.txt](https://github.com/textpattern/textpattern/blob/main/README.txt) for details on upgrading Textpattern. +Please see [UPGRADE.txt](https://github.com/textpattern/textpattern/blob/main/UPGRADE.txt) for details on upgrading Textpattern. ## Help and Support -The [Textpattern support forum](https://forum.textpattern.com) is home to a friendly and helpful community of Textpattern users and experts. Textpattern also has a social network presence on [Mastodon](https://textpattern.com/mastodon) and [Twitter](https://textpattern.com/twitter). +The [Textpattern support forum](https://forum.textpattern.com) is home to a friendly and helpful community of Textpattern users and experts. Textpattern also has a social network presence on [Mastodon](https://textpattern.com/mastodon) and [X](https://textpattern.com/x). ## Development @@ -62,20 +63,26 @@ The following table outlines anticipated forthcoming changes to system requireme #### Textpattern development versions -Note that targeted versions listed may change multiple times during the development process. +We are targeting Textpattern 5 as the next major release. Refer to the following table for anticipated changes to system requirements for Textpattern 5. -We are targeting Textpattern 4.9 as the next minor release. Refer to the following table for anticipated changes to system requirements. +We generally recommend running Textpattern on platforms with active vendor support where possible, though we also maintain a minimum system requirements list for situations where that isn't viable. + +Note that versions listed may change multiple times during the development process. | | Minimum | Recommended | |--------|:-------:|:-----:| -| PHP | 5.6 | [vendor supported](https://php.net/supported-versions.php)
(8.0, 8.1 or 8.2) | -| MySQL | 5.5 | [vendor supported](https://www.mysql.com/support/supportedplatforms/database.html)
(5.7 and/or 8.0, depends on platform) | -| Apache | — | vendor supported
(2.4) | -| Nginx | — | mainline (1.23) or stable (1.22) | +| PHP | — | [vendor supported](https://php.net/supported-versions.php) | +| MySQL | — | [vendor supported LTS](https://www.mysql.com/support/supportedplatforms/database.html) | +| Apache | — | vendor supported | +| Nginx | — | mainline or stable | ## Contributing -Do you want to help with the development of Textpattern? Please refer to the [contributing documentation](https://github.com/textpattern/textpattern/blob/dev/.github/CONTRIBUTING.md) for full details. +Please refer to the [contributing documentation](https://github.com/textpattern/textpattern/blob/dev/CONTRIBUTING.md) for more details of Textpattern development. + +## Additional development tools + +Please refer to the [additional devevelopment tools](https://github.com/textpattern/textpattern/blob/dev/DEVTOOLS.md) document. ## GitHub topic tags @@ -87,53 +94,9 @@ If you use GitHub for Textpattern-related development please consider adding som * [`textpattern-website`](https://github.com/topics/textpattern-website) (for websites built with Textpattern) * [`textpattern-development`](https://github.com/topics/textpattern-development) (for development resources) -## Additional development tools - -Various components used within Textpattern (such as the bundled themes and language translations) are maintained in standalone repositories. Textpattern has a simple development toolset built on [Node.js](https://nodejs.org/) to pull the distribution files of those repositories into the core as required. - -You can install Node.js using the [installer](https://nodejs.org/en/download/) or [package manager](https://nodejs.org/en/download/package-manager/). - -Install required dev tools: - -```ShellSession -npm install -``` - -You can then pull the following components from the CLI, like so: - -```ShellSession -npm run get-default-theme -npm run get-classic-admin-theme -npm run get-hive-admin-theme -npm run get-pophelp -npm run get-textpacks -npm run get-dependencies -``` - -To request a specific tag or branch: - -```ShellSession -npm run get-default-theme 4.8.8 -npm run get-classic-admin-theme 4.8.8 -npm run get-classic-admin-theme 4.8.x -npm run get-hive-admin-theme 4.8.x -npm run get-textpacks 4.8.x -``` - -Release tools: - -Usage: `npm run txp-gitdist [dest-dir]` (`dest-dir` defaults to a -temporary location). - -```ShellSession -npm run txp-index -npm run txp-checksums -npm run txp-gitdist 1.2.3 ../my-dest-dir -``` - ## Thank You -Thank you to our [GitHub monthly sponsors](https://github.com/sponsors/textpattern). Your continued support is greatly appreciated! +Thank you to our [GitHub sponsors](https://github.com/sponsors/textpattern). Your continued support is greatly appreciated! We are grateful to [DigitalOcean](https://www.digitalocean.com/?utm_source=opensource&utm_campaign=textpattern), [BrowserStack](https://www.browserstack.com) and [1Password](https://1password.com) for their kind considerations in supporting Textpattern CMS development by way of web hosting infrastructure (DigitalOcean), cross-browser testing platform (BrowserStack) and secure password management (1Password). Thank you! diff --git a/textpattern/include/txp_article.php b/textpattern/include/txp_article.php index b59c239f40..6899d9c482 100644 --- a/textpattern/include/txp_article.php +++ b/textpattern/include/txp_article.php @@ -153,23 +153,6 @@ function article_save() UNIX_TIMESTAMP(Posted) AS sPosted, UNIX_TIMESTAMP(Expires) AS sExpires", 'textpattern', "ID = ".(int) $incoming['ID']); - - if (!($oldArticle['Status'] >= STATUS_LIVE && has_privs('article.edit.published') - || $oldArticle['Status'] >= STATUS_LIVE && $oldArticle['AuthorID'] === $txp_user && has_privs('article.edit.own.published') - || $oldArticle['Status'] < STATUS_LIVE && has_privs('article.edit') - || $oldArticle['Status'] < STATUS_LIVE && $oldArticle['AuthorID'] === $txp_user && has_privs('article.edit.own'))) { - // Not allowed, you silly rabbit, you shouldn't even be here. - // Show default editing screen. - article_edit(); - - return; - } - - if ($oldArticle['sLastMod'] != $incoming['sLastMod']) { - article_edit(array(gTxt('concurrent_edit_by', array('{author}' => txpspecialchars($oldArticle['LastModID']))), E_ERROR), true, true); - - return; - } } else { $oldArticle = array('Status' => STATUS_PENDING, 'url_title' => '', @@ -183,6 +166,26 @@ function article_save() ); } + $wasPublished = has_status_group($oldArticle['Status'], 'published'); + $wasUnpublished = has_status_group($oldArticle['Status'], 'unpublished'); + + if (!(($wasPublished && has_privs('article.edit.published')) + || ($wasPublished && $oldArticle['AuthorID'] === $txp_user && has_privs('article.edit.own.published')) + || ($wasUnpublished && has_privs('article.edit')) + || ($wasUnpublished && $oldArticle['AuthorID'] === $txp_user && has_privs('article.edit.own')))) { + // Not allowed, you silly rabbit, you shouldn't even be here. + // Show default editing screen. + article_edit(); + + return; + } + + if ($oldArticle['sLastMod'] != $incoming['sLastMod']) { + article_edit(array(gTxt('concurrent_edit_by', array('{author}' => txpspecialchars($oldArticle['LastModID']))), E_ERROR), true, true); + + return; + } + if (!has_privs('article.set_markup')) { $incoming['textile_body'] = $oldArticle['textile_body']; $incoming['textile_excerpt'] = $oldArticle['textile_excerpt']; @@ -194,7 +197,7 @@ function article_save() $ID = intval($ID); $Status = assert_int($Status); - if (!has_privs('article.publish') && $Status >= STATUS_LIVE) { + if (!has_privs('article.publish') && has_status_group($Status, 'published')) { $Status = STATUS_PENDING; } @@ -273,7 +276,7 @@ function article_save() // Auto-update custom-titles according to Title, as long as unpublished and // NOT customised. if (empty($url_title) - || (($oldArticle['Status'] < STATUS_LIVE) + || (($wasUnpublished) && ($oldArticle['url_title'] === $url_title) && ($oldArticle['Title'] !== $Title) && ($oldArticle['url_title'] === stripSpace($oldArticle['Title'], 1)) @@ -339,12 +342,14 @@ function article_save() ); } - if ($Status >= STATUS_LIVE) { - if ($oldArticle['Status'] < STATUS_LIVE) { - do_pings(); - } else { - update_lastmod($ID ? 'article_saved' : 'article_posted', $rs); - } + $isNowPublished = has_status_group($Status, 'published'); + + if ($isNowPublished && $wasUnpublished) { + do_pings(); + } + + if ($isNowPublished || $wasPublished) { + update_lastmod($ID ? 'article_saved' : 'article_posted', $rs); } now('posted', true); @@ -748,7 +753,7 @@ function article_edit($message = '', $concurrent = false, $refresh_partials = fa $response[] = announce($message); $response[] = '$("#article_form [type=submit]").val(textpattern.gTxt("save"))'; - if ($Status < STATUS_LIVE) { + if (has_status_group($Status, 'unpublished')) { $response[] = '$("#article_form").addClass("saved").removeClass("published")'; } else { $response[] = '$("#article_form").addClass("published").removeClass("saved")'; @@ -773,7 +778,7 @@ function article_edit($message = '', $concurrent = false, $refresh_partials = fa $class = array('async'); - if ($Status >= STATUS_LIVE) { + if (has_status_group($Status, 'published')) { $class[] = 'published'; } elseif ($ID) { $class[] = 'saved'; @@ -1393,7 +1398,7 @@ function article_partial_actions($rs) $push_button = ''; if (empty($rs['ID'])) { - if (has_privs('article.publish') && get_pref('default_publish_status', STATUS_LIVE) >= STATUS_LIVE) { + if (has_privs('article.publish') && has_status_group(get_pref('default_publish_status', STATUS_LIVE), 'published')) { $push_button = fInput('submit', 'publish', gTxt('publish'), 'publish'); } else { $push_button = fInput('submit', 'publish', gTxt('save'), 'publish'); @@ -1401,10 +1406,10 @@ function article_partial_actions($rs) $push_button = graf($push_button, array('class' => 'txp-save')); } elseif ( - ($rs['Status'] >= STATUS_LIVE && has_privs('article.edit.published')) || - ($rs['Status'] >= STATUS_LIVE && $rs['AuthorID'] === $txp_user && has_privs('article.edit.own.published')) || - ($rs['Status'] < STATUS_LIVE && has_privs('article.edit')) || - ($rs['Status'] < STATUS_LIVE && $rs['AuthorID'] === $txp_user && has_privs('article.edit.own')) + (($isPublished = has_status_group($rs['Status'], 'published')) && has_privs('article.edit.published')) || + ($isPublished && $rs['AuthorID'] === $txp_user && has_privs('article.edit.own.published')) || + (($isUnpublished = has_status_group($rs['Status'], 'unpublished')) && has_privs('article.edit')) || + ($isUnpublished && $rs['AuthorID'] === $txp_user && has_privs('article.edit.own')) ) { $push_button = graf(fInput('submit', 'save', gTxt('save'), 'publish'), array('class' => 'txp-save')); } @@ -1658,7 +1663,7 @@ function article_partial_article_view($rs) { extract($rs); - if ($Status != STATUS_LIVE and $Status != STATUS_STICKY) { + if (has_status_group($Status, 'unpublished')) { if (!has_privs('article.preview')) { return; } diff --git a/textpattern/include/txp_file.php b/textpattern/include/txp_file.php index 9378e2aca6..0332718147 100644 --- a/textpattern/include/txp_file.php +++ b/textpattern/include/txp_file.php @@ -45,7 +45,7 @@ ); global $file_statuses; -$file_statuses = status_list(true, array(STATUS_DRAFT, STATUS_STICKY)); +$file_statuses = status_group('files', true); if ($event == 'file') { require_privs('file'); @@ -662,7 +662,7 @@ function file_edit($message = '', $id = '') $permissions = '-1'; } - if (!has_privs('file.publish') && $status >= STATUS_LIVE) { + if (!has_privs('file.publish') && has_status_group($status, 'published')) { $status = STATUS_PENDING; } diff --git a/textpattern/include/txp_list.php b/textpattern/include/txp_list.php index 2ddfe48b4b..333aed34aa 100644 --- a/textpattern/include/txp_list.php +++ b/textpattern/include/txp_list.php @@ -364,7 +364,7 @@ function list_list($message = '', $post = '') $Category1 = ($Category1) ? span(txpspecialchars($category1_title), array('title' => $Category1)) : ''; $Category2 = ($Category2) ? span(txpspecialchars($category2_title), array('title' => $Category2)) : ''; - if ($Status != STATUS_LIVE and $Status != STATUS_STICKY) { + if (has_status_group($Status, 'unpublished')) { $view_url = $can_preview ? '?txpreview='.intval($ID).'.'.time() : ''; } else { $view_url = permlinkurl($a); @@ -403,12 +403,10 @@ function list_list($message = '', $post = '') $contentBlock .= tr( td( ( - ( - ($a['Status'] >= STATUS_LIVE and has_privs('article.edit.published')) - or ($a['Status'] >= STATUS_LIVE and $AuthorID === $txp_user and has_privs('article.edit.own.published')) - or ($a['Status'] < STATUS_LIVE and has_privs('article.edit')) - or ($a['Status'] < STATUS_LIVE and $AuthorID === $txp_user and has_privs('article.edit.own')) - ) + ((($isPublished = has_status_group($a['Status'], 'published')) and has_privs('article.edit.published')) + or ($isPublished and $AuthorID === $txp_user and has_privs('article.edit.own.published')) + or (($isUnpublished = has_status_group($a['Status'], 'unpublished')) and has_privs('article.edit')) + or ($isUnpublished and $AuthorID === $txp_user and has_privs('article.edit.own'))) ? fInput('checkbox', 'selected[]', $ID, 'checkbox') : '' ), '', 'txp-list-col-multi-edit' @@ -665,7 +663,7 @@ function list_multi_edit() $field = 'Status'; } - if (!has_privs('article.publish') && $value >= STATUS_LIVE) { + if (!has_privs('article.publish') && has_status_group($value, 'published')) { $value = STATUS_PENDING; } break; @@ -681,10 +679,10 @@ function list_multi_edit() foreach ($selected as $item) { if ( - ($item['Status'] >= STATUS_LIVE && has_privs('article.edit.published')) || - ($item['Status'] >= STATUS_LIVE && $item['AuthorID'] === $txp_user && has_privs('article.edit.own.published')) || - ($item['Status'] < STATUS_LIVE && has_privs('article.edit')) || - ($item['Status'] < STATUS_LIVE && $item['AuthorID'] === $txp_user && has_privs('article.edit.own')) + (($isPublished = has_status_group($item['Status'], 'published')) && has_privs('article.edit.published')) || + ($isPublished && $item['AuthorID'] === $txp_user && has_privs('article.edit.own.published')) || + (($isUnpublished = has_status_group($item['Status'], 'unpublished')) && has_privs('article.edit')) || + ($isUnpublished && $item['AuthorID'] === $txp_user && has_privs('article.edit.own')) ) { $allowed[] = $item['ID']; } @@ -710,7 +708,7 @@ function list_multi_edit() $a['uid'] = md5(uniqid(rand(), true)); $a['AuthorID'] = $txp_user; $a['LastModID'] = $txp_user; - $a['Status'] = ($a['Status'] >= STATUS_LIVE) ? STATUS_DRAFT : $a['Status']; + $a['Status'] = (has_status_group($a['Status'], 'published')) ? STATUS_DRAFT : $a['Status']; foreach ($a as $name => &$value) { if ($name == 'Expires' && !$value) { diff --git a/textpattern/lib/txplib_misc.php b/textpattern/lib/txplib_misc.php index 64e8b0242f..c9283947ea 100644 --- a/textpattern/lib/txplib_misc.php +++ b/textpattern/lib/txplib_misc.php @@ -6003,6 +6003,78 @@ function status_list($labels = true, $exclude = array()) return $status_list; } +/** + * Return a list of status codes and their associated names by group. + * + * The groups can be extended with a 'status.types > groups' callback event. + * Callback functions get passed three arguments: '$event', '$step' and + * '$status_list'. The third parameter contains a nested array of $group => + * 'status_code => label' pairs. + * + * Note the unpublished statuses are not in numerical order, by design. + * This facilitates plugins that may wish to introduce a 'publisher workflow'. + * Such plugins could define a new group and/or could introduce a + * 'next status' feature which might step through the status codes within + * a group to indicate a document's flow through an editorial chain. + * Hidden->Draft->Pending (i.e. draft, then review by an editor prior to + * publication) is a logical default workflow. + * + * @param string The group to return + * @param bool Return the list with L10n labels (for UI purposes) or raw values (for comparisons) + * @return array A status array + * @since 4.7.0 + */ + +function status_group($group = 'published', $labels = true) +{ + $status_list = array( + 'unpublished' => array( + STATUS_HIDDEN => 'hidden', + STATUS_DRAFT => 'draft', + STATUS_PENDING => 'pending', + ), + 'published' => array( + STATUS_LIVE => 'live', + STATUS_STICKY => 'sticky', + ), + 'files' => array( + STATUS_HIDDEN => 'hidden', + STATUS_PENDING => 'pending', + STATUS_LIVE => 'live', + ), + ); + + callback_event_ref('status.types', 'groups', 0, $status_list); + + $outList = array(); + + if (array_key_exists($group, $status_list)) { + $outList = $status_list[$group]; + } + + if ($labels) { + $outList = array_map('gTxt', $outList); + } + + return $outList; +} + +/** + * Determine if the passed $status is in the nominated $group. + * + * @param int $status Status code + * @param string $group Group name + * @return boolean + * @since 4.7.0 + */ + +function has_status_group($status, $group) +{ + $statuses = status_group($group, false); + + return array_key_exists($status, $statuses); +} + /** * Translates article status names into numerical status codes. * diff --git a/textpattern/lib/txplib_publish.php b/textpattern/lib/txplib_publish.php index 19b8ab447e..8b05b51c0b 100644 --- a/textpattern/lib/txplib_publish.php +++ b/textpattern/lib/txplib_publish.php @@ -1003,7 +1003,7 @@ function filterAtts($atts = null, $iscustom = null) } if ($q && $searchsticky) { - $statusq = " AND Status >= ".STATUS_LIVE; + $statusq = " AND Status IN (".implode(',', array_keys(status_group('published', false))).")"; } else { $statusq = " AND Status IN (".implode(',', $status).")"; } diff --git a/textpattern/publish.php b/textpattern/publish.php index b33a462d3a..f0a913e110 100644 --- a/textpattern/publish.php +++ b/textpattern/publish.php @@ -651,9 +651,9 @@ function preText($store, $prefs = null) if (!$is_404 && $id && $out['s'] !== 'file_download') { if (empty($thisarticle)) { $a = safe_row( - "*, UNIX_TIMESTAMP(Posted) AS uPosted, UNIX_TIMESTAMP(Expires) AS uExpires, UNIX_TIMESTAMP(LastMod) AS uLastMod", - 'textpattern', - "ID = $id".(gps('txpreview') ? '' : " AND Status IN (".STATUS_LIVE.",".STATUS_STICKY.")") + "*, UNIX_TIMESTAMP(Posted) AS uPosted, UNIX_TIMESTAMP(Expires) AS uExpires, UNIX_TIMESTAMP(LastMod) AS uLastMod", + 'textpattern', + "ID = $id".(gps('txpreview') ? '' : " AND Status IN (".implode(',', array_keys(status_group('published', false))).")") ); if ($a) { diff --git a/textpattern/publish/taghandlers.php b/textpattern/publish/taghandlers.php index 498f459f42..602e7e2f82 100644 --- a/textpattern/publish/taghandlers.php +++ b/textpattern/publish/taghandlers.php @@ -1131,7 +1131,7 @@ function recent_comments($atts, $thing = null) $rs = startRows("SELECT d.name, d.email, d.web, d.message, d.discussid, UNIX_TIMESTAMP(d.Posted) AS time, t.ID AS thisid, UNIX_TIMESTAMP(t.Posted) AS posted, t.Title AS title, t.Section AS section, t.Category1, t.Category2, t.url_title FROM ".safe_pfx('txp_discuss')." AS d INNER JOIN ".safe_pfx('textpattern')." AS t ON d.parentid = t.ID - WHERE t.Status >= ".STATUS_LIVE.$expired." AND d.visible = ".VISIBLE." + WHERE t.Status IN(".implode(',', array_keys(status_group('published', false))).")".$expired." AND d.visible = ".VISIBLE." ORDER BY ".sanitizeForSort($sort)." LIMIT ".intval($offset).", ".($limit ? intval($limit) : PHP_INT_MAX));