Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugins/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,4 @@ the packaged plugin (`pkg_build.sh` only ships `source/community.applications/`)
- Fixed: On Spotlight app cards, a long application name no longer runs underneath the "Monthly Spotlight" badge
- Changed: Long application names on app cards now wrap to a second line instead of being cut off with an ellipsis
- Removed: The category line on app cards (the category is still shown in the app panel)
- Fixed: Prevented rare Apps page errors and log noise caused by corrupted cache files, malformed application-feed data, or unusual template and settings values; pinned apps now also respect the Hide Deprecated setting
Original file line number Diff line number Diff line change
Expand Up @@ -835,18 +835,9 @@ $(function(){
caSessRemove("ca_languageSwitch");
}

caPluginUpdateCheck("community.applications.plg",{
noDismiss:true,
name:"Community Applications",
debug:false,
priority:true
},function() {
/* Runs for the update-check side effect only. The CA version used to
be written into the menu's #caInstalledVersion here, but the version
now renders server-side at the top of the Credits panel, so there's
nothing to display from the callback. */
}
);
/* CA self-update check moved to after the first Action Centre response
(see setupActionCentre) so a CA plugin check kicked off here can't leave
enableActionCentre blocking on an update that this same load started. */


searchBoxInput = $("#searchBox").get(0);
Expand Down Expand Up @@ -5723,6 +5714,17 @@ function setupActionCentre() {
}
} else
$(".actionCentre").hide();

/* Kick the CA self update check once, only after the first Action Centre response returns. */
if ( ! window.caPluginUpdateChecked ) {
window.caPluginUpdateChecked = true;
caPluginUpdateCheck("community.applications.plg",{
noDismiss:true,
name:"Community Applications",
debug:false,
priority:true
},function() {});
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1968,7 +1968,7 @@ function pinApp() {

$repository = getPost("repository","oops");
$name = getPost("name","oops");
$pinnedApps = readJsonFile(CA_PATHS['pinnedV2']);
$pinnedApps = is_array($p = readJsonFile(CA_PATHS['pinnedV2'])) ? $p : [];
if (isset($pinnedApps["$repository&$name"]) )
$pinnedApps["$repository&$name"] = false;
else
Expand All @@ -1985,7 +1985,7 @@ function pinApp() {
*/
function areAppsPinned() {

postReturn(['status' => in_array(true,readJsonFile(CA_PATHS['pinnedV2']))]);
postReturn(['status' => in_array(true, is_array($p = readJsonFile(CA_PATHS['pinnedV2'])) ? $p : [])]);
}

/**
Expand Down Expand Up @@ -2016,13 +2016,14 @@ function pinnedApps() {

$displayed = [];
$hideIncompatible = ($GLOBALS['caSettings']['hideIncompatible'] ?? "false") === "true";
$hideDeprecated = ($GLOBALS['caSettings']['hideDeprecated'] ?? "false") === "true";

foreach ($pinnedApps as $pinned) {
if (!is_string($pinned) || strpos($pinned, '&') === false) {
continue;
}

$template = PinnedAppsHelpers::findPinnedTemplate($templates, $pinned, $hideIncompatible);
$template = PinnedAppsHelpers::findPinnedTemplate($templates, $pinned, $hideIncompatible, $hideDeprecated);
if ($template !== null) {
$displayed[] = $template;
}
Expand Down Expand Up @@ -2993,6 +2994,10 @@ function createXML() {
}

$xml = makeXML($template);
if ( $xml === false ) {
postReturn(["error" => tr("Could not build the template XML (invalid characters in the template)")]);
return;
}
@mkdir(dirname($xmlFile),0777,true);
ca_file_put_contents($xmlFile,$xml);
} elseif ( $type === "user" ) {
Expand Down Expand Up @@ -3317,6 +3322,10 @@ function convert_docker() {
}

$dockerXML = makeXML($dockerfile);
if ( $dockerXML === false ) {
postReturn(["error" => tr("Could not build the container XML (invalid characters in the template)")]);
return;
}

/* Per-request output path so two concurrent convert_docker calls (e.g. two
tabs) don't clobber each other's redirect target. Sweeping happens in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function getGlobals() {
clearstatcache();
if ( is_file(CA_PATHS['community-templates-info']) ) {
if ( ! isset($GLOBALS['templates']) ) {
$GLOBALS['templates'] = readJsonFile(CA_PATHS['community-templates-info']);
$GLOBALS['templates'] = is_array($t = readJsonFile(CA_PATHS['community-templates-info'])) ? $t : [];
}
} else {
$GLOBALS['templates'] = [];
Expand All @@ -80,7 +80,7 @@ function getGlobals() {
* @return void
*/
function getFullGlobals() {
$GLOBALS['templates'] = readJsonFile(CA_PATHS['community-templates-info-full']);
$GLOBALS['templates'] = is_array($t = readJsonFile(CA_PATHS['community-templates-info-full'])) ? $t : [];
getSettings();
}

Expand Down Expand Up @@ -1155,7 +1155,8 @@ function fixTemplates($template) {
* sanitizes Requires links, and delegates to Array2XML.
*
* @param array<string,mixed> $template
* @return string XML document.
* @return string|false XML document, or false when the template contains
* characters invalid in XML tag/attribute names.
*/
function makeXML($template) {
# ensure its a v2 template if the Config entries exist
Expand All @@ -1178,7 +1179,12 @@ function makeXML($template) {
}
}
$Array2XML = new Array2XML();
$xml = $Array2XML->createXML("Container",$template);
try {
$xml = $Array2XML->createXML("Container",$template);
} catch (Throwable $e) {
debug("makeXML: Array2XML failed for ".($template['Name'] ?? 'unknown')." - ".$e->getMessage());
return false;
}
Comment thread
Squidly271 marked this conversation as resolved.
return $xml->saveXML();
}
/**
Expand Down Expand Up @@ -1340,6 +1346,7 @@ function moderateTemplates() {
$templates = &$GLOBALS['templates'];

if ( ! $templates ) return;
$o = [];
foreach ($templates as $template) {
$template['Compatible'] = versionCheck($template);
if ( ($template['MaxVer']??null) && version_compare($template['MaxVer'],$GLOBALS['caSettings']['unRaidVersion']) < 0 )
Expand Down Expand Up @@ -1927,6 +1934,12 @@ function postReturn($retArray) {
* @return string
*/
function tr($string,$options=-1) {
// dynamix _() runs trim() on its argument immediately, which TypeErrors
// on an array and deprecation-warns on null under PHP 8. Coerce scalars
// to string and collapse anything else to empty so a stray non-string
// (eg. a malformed feed field) can never fatal in the translator.
if ( ! is_string($string) )
$string = is_scalar($string) ? (string)$string : "";
$translated = _($string,$options);
if ( ! trim($translated) )
$translated = $string;
Expand Down Expand Up @@ -1967,7 +1980,7 @@ function languageCheck($template) {
$xmlFile = readXmlFile($installedLanguage,true);

if ( !$xmlFile['Version'] ) return false;
return (strcmp($template['Version'],$xmlFile['Version']) > 0) || (strcmp($OSupdates['Version'],$xmlFile['Version']) > 0);
return (strcmp((string)($template['Version'] ?? ''),$xmlFile['Version']) > 0) || (strcmp($OSupdates['Version'],$xmlFile['Version']) > 0);
}
/**
* Serialize an associative array (with optional one-level sections) to INI on disk.
Expand Down Expand Up @@ -2020,8 +2033,15 @@ function getAllInfo($force=false) {
global $DockerTemplates, $DockerClient;

$containers = readJsonFile(CA_PATHS['info']);
// Normalize a corrupt cache (readJsonFile can decode a scalar) to [] right
// here, before the refresh decision. Otherwise a truthy scalar leaves both
// empty() and the refresh path false, so we would return [] on every
// non-forced call without ever rebuilding from Docker.
if ( ! is_array($containers) ) {
$containers = [];
}

if ( $force || ! $containers || empty($containers) ) {
if ( $force || empty($containers) ) {
if ( caIsDockerRunning() ) {
$info = $DockerTemplates->getAllInfo(false,true,true);
$containers = $DockerClient->getDockerContainers();
Expand All @@ -2037,6 +2057,9 @@ function getAllInfo($force=false) {
} else {
debug("Cached info update");
}
// $containers is guaranteed an array here (normalized above; the refresh
// branch reassigns it from getDockerContainers()), so callers with typed
// array $info params never receive a scalar.
return $containers;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static function clearPinnedCacheFiles($cacheKeys) {
* @param bool $hideIncompatible When true, skip templates that aren't compatible.
* @return array<string,mixed>|null Matching template, or null when nothing matches.
*/
public static function findPinnedTemplate(&$templates, $pinned, $hideIncompatible) {
public static function findPinnedTemplate(&$templates, $pinned, $hideIncompatible, $hideDeprecated = false) {
$search = explode("&", $pinned);
if (count($search) < 2) {
return null;
Expand Down Expand Up @@ -67,6 +67,11 @@ public static function findPinnedTemplate(&$templates, $pinned, $hideIncompatibl
continue;
}

if ($hideDeprecated && !empty($template['Deprecated'])) {
$startIndex = $index + 1;
continue;
}

return $template;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -585,8 +585,11 @@ public static function init($version = '1.0', $encoding = 'UTF-8', $format_outpu
*/
public static function &createXML($node_name, $arr=array()) {
$xml = self::getXMLRoot();
$xml->appendChild(self::convert($node_name, $arr));
self::$xml = null; // clear the xml node in the class for 2nd time use.
try {
$xml->appendChild(self::convert($node_name, $arr));
} finally {
self::$xml = null; // clear the xml node in the class for 2nd time use, even on throw.
}
return $xml;
}
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1162,7 +1162,7 @@ function my_display_apps($file,$pageNumber=1,$selectedApps=false,$startup=false,
$repositories = readJsonFile(CA_PATHS['repositoryList']);
$extraBlacklist = readJsonFile(CA_PATHS['extraBlacklist']);
$extraDeprecated = readJsonFile(CA_PATHS['extraDeprecated']);
$pinnedApps = readJsonFile(CA_PATHS['pinnedV2']);
$pinnedApps = is_array($p = readJsonFile(CA_PATHS['pinnedV2'])) ? $p : [];

$ct = "";
$cardsArray = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -565,11 +565,11 @@ function caBuildActionsContext(array &$template, array $info, array $dockerUpdat
$actionsContext[] = ["icon"=>"ca_fa-install","text"=>tr("Install second"),"action"=>"popupInstallXML('".addslashes($template['Path'])."','second');"];
}
}
if (is_file($info[$name]['template'])) {
if (is_file($info[$name]['template'] ?? "")) {
$actionsContext[] = ["icon"=>"ca_fa-edit","text"=>tr("Edit"),"action"=>"popupInstallXML('".addslashes($info[$name]['template'])."','edit');"];
}
$actionsContext[] = ["divider"=>true];
if ($info[$name]['template']) {
if ($info[$name]['template'] ?? false) {
$actionsContext[] = ["icon"=>"ca_fa-delete","text"=>tr("Uninstall"),"action"=>"uninstallDocker('".addslashes($info[$name]['template'])."','{$template['Name']}');"];
$template['Installed'] = true;
}
Expand Down Expand Up @@ -727,7 +727,7 @@ function caBuildLanguageActions(array &$template, ?string $countryCode, array $a
$actionsContext[] = ["icon"=>"ca_fa-switchto","text"=>$template['SwitchLanguage'],"action"=>"CAswitchLanguage('$countryCode');"];
}
} else {
$actionsContext[] = ["icon"=>"ca_fa-install","text"=>tr("Install"),"action"=>"installLanguage('{$template['TemplateURL']}','$countryCode');"];
$actionsContext[] = ["icon"=>"ca_fa-install","text"=>tr("Install"),"action"=>"installLanguage('".($template['TemplateURL'] ?? '')."','$countryCode');"];
}

if (file_exists("/var/log/plugins/lang-$countryCode.xml")) {
Expand Down Expand Up @@ -909,7 +909,7 @@ function caProcessDockerTemplate(array $template, array $info, array $dockerUpda
$template['Installed'] = $selected;
if ($selected) {
$ind = searchArray($info, "Name", $name);
if ($info[$ind]['url'] && $info[$ind]['running']) {
if ($ind !== false && ($info[$ind]['url'] ?? false) && ($info[$ind]['running'] ?? false)) {
$actionsContext[] = ["icon" => "ca_fa-globe", "text" => tr("WebUI"), "action" => "openNewWindow('{$info[$ind]['url']}','_blank');"];
if ($info[$ind]['TSurl'] ?? false) {
$actionsContext[] = ["icon" => "ca_fa-globe", "text" => tr("TS WebUI"), "action" => "openNewWindow('{$info[$ind]['TSurl']}','_blank');"];
Expand Down Expand Up @@ -941,12 +941,12 @@ function caProcessDockerTemplate(array $template, array $info, array $dockerUpda
}
}

if (is_file($info[$ind]['template'])) {
if ($ind !== false && is_file($info[$ind]['template'] ?? "")) {
$actionsContext[] = ["icon" => "ca_fa-edit", "text" => tr("Edit"), "action" => "popupInstallXML('".addslashes($info[$ind]['template'])."','edit');"];
}

$actionsContext[] = ["divider" => true];
if ($info[$ind]['template']) {
if ($ind !== false && ($info[$ind]['template'] ?? false)) {
$actionsContext[] = ["icon" => "ca_fa-delete", "text" => tr("Uninstall"), "action" => "uninstallDocker('".addslashes($info[$ind]['template'])."','{$template['Name']}');"];
}
} elseif (! ($template['Blacklist'] ?? false)) {
Expand Down Expand Up @@ -1127,7 +1127,7 @@ function caProcessLanguageTemplate(array $template, array $actionsContext): arra
$actionsContext[] = ["icon" => "ca_fa-switchto", "text" => $template['SwitchLanguage'], "action" => "CAswitchLanguage('$countryCode');"];
}
} else {
$actionsContext[] = ["icon" => "ca_fa-install", "text" => tr("Install"), "action" => "installLanguage('{$template['TemplateURL']}','$countryCode');"];
$actionsContext[] = ["icon" => "ca_fa-install", "text" => tr("Install"), "action" => "installLanguage('".($template['TemplateURL'] ?? '')."','$countryCode');"];
}

if (file_exists("/var/log/plugins/lang-$countryCode.xml")) {
Expand Down
Loading